Skip to content

Commit 828a4e7

Browse files
Tarteelclaude
andcommitted
feat: private room creator panel + two-step invite modal
- Room page: creator sees "Start Prayer Now" button and friends invite list in waiting/building states; non-creator sees "Waiting for host" - Dashboard: create modal transitions to invite step after room is created — shows room link and per-friend Invite buttons - backend/schemas/room.py: expose creator_id in RoomSlotResponse - frontend/lib/api.ts: add creator_id to RoomSlot type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a90b732 commit 828a4e7

File tree

4 files changed

+307
-87
lines changed

4 files changed

+307
-87
lines changed

backend/schemas/room.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class RoomSlotResponse(BaseModel):
1818
started_at: datetime | None
1919
ended_at: datetime | None
2020
is_private: bool = False
21+
creator_id: uuid.UUID | None = None
2122
invite_code: str | None = None
2223

2324
class Config:

frontend/app/dashboard/page.tsx

Lines changed: 153 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import { useEffect, useState, useCallback } from "react";
33
import { useRouter } from "next/navigation";
4-
import { roomsApi, usersApi, privateRoomsApi, TonightRooms, PrivateRoom } from "@/lib/api";
4+
import { roomsApi, usersApi, privateRoomsApi, friendsApi, TonightRooms, PrivateRoom, Friend } from "@/lib/api";
55
import { useAuthStore } from "@/lib/auth";
66
import CountdownTimer from "@/components/CountdownTimer";
77
import RoomCard from "@/components/RoomCard";
@@ -31,6 +31,11 @@ export default function DashboardPage() {
3131
const [showCreateModal, setShowCreateModal] = useState(false);
3232
const [createForm, setCreateForm] = useState({ rakats: 8, juz_number: 1, juz_per_night: 1.0 });
3333
const [creating, setCreating] = useState(false);
34+
// Post-creation invite step
35+
const [createdRoom, setCreatedRoom] = useState<{ id: string; room_url: string } | null>(null);
36+
const [friends, setFriends] = useState<Friend[]>([]);
37+
const [inviteBusy, setInviteBusy] = useState<Record<string, boolean>>({});
38+
const [inviteDone, setInviteDone] = useState<Record<string, boolean>>({});
3439

3540
const handleUnauth = useCallback(() => {
3641
clearAuth();
@@ -308,81 +313,161 @@ export default function DashboardPage() {
308313
{showCreateModal && (
309314
<div className="fixed inset-0 z-50 flex items-center justify-center px-4 bg-black/60 backdrop-blur-sm">
310315
<div className="w-full max-w-sm glass-card p-6 mosque-glow space-y-4">
311-
<div className="flex items-center justify-between">
312-
<h2 className="font-bold text-white">Create Private Room</h2>
313-
<button onClick={() => setShowCreateModal(false)} className="text-gray-500 hover:text-gray-300"></button>
314-
</div>
315316

316-
<div className="space-y-3">
317-
<div>
318-
<label className="text-xs text-gray-400 mb-1 block">Rakats</label>
319-
<div className="flex gap-2">
320-
{[8, 20].map((r) => (
321-
<button
322-
key={r}
323-
onClick={() => setCreateForm((f) => ({ ...f, rakats: r }))}
324-
className={`flex-1 py-2 rounded-lg border text-sm font-medium transition-colors ${
325-
createForm.rakats === r
326-
? "border-mosque-gold bg-mosque-gold/10 text-mosque-gold"
327-
: "border-gray-700 text-gray-400"
328-
}`}
317+
{/* Step 1 — configure room */}
318+
{!createdRoom ? (
319+
<>
320+
<div className="flex items-center justify-between">
321+
<h2 className="font-bold text-white">Create Private Room</h2>
322+
<button onClick={() => setShowCreateModal(false)} className="text-gray-500 hover:text-gray-300"></button>
323+
</div>
324+
325+
<div className="space-y-3">
326+
<div>
327+
<label className="text-xs text-gray-400 mb-1 block">Rakats</label>
328+
<div className="flex gap-2">
329+
{[8, 20].map((r) => (
330+
<button
331+
key={r}
332+
onClick={() => setCreateForm((f) => ({ ...f, rakats: r }))}
333+
className={`flex-1 py-2 rounded-lg border text-sm font-medium transition-colors ${
334+
createForm.rakats === r
335+
? "border-mosque-gold bg-mosque-gold/10 text-mosque-gold"
336+
: "border-gray-700 text-gray-400"
337+
}`}
338+
>
339+
{r} Rakats
340+
</button>
341+
))}
342+
</div>
343+
</div>
344+
345+
<div>
346+
<label className="text-xs text-gray-400 mb-1 block">Juz</label>
347+
<select
348+
value={createForm.juz_number}
349+
onChange={(e) => setCreateForm((f) => ({ ...f, juz_number: Number(e.target.value) }))}
350+
className="w-full bg-mosque-darkest border border-white/10 rounded-lg px-3 py-2 text-white text-sm outline-none focus:border-mosque-gold/50"
329351
>
330-
{r} Rakats
331-
</button>
332-
))}
352+
{Array.from({ length: 30 }, (_, i) => i + 1).map((n) => (
353+
<option key={n} value={n}>Juz {n}</option>
354+
))}
355+
</select>
356+
</div>
357+
358+
<div>
359+
<label className="text-xs text-gray-400 mb-1 block">Amount</label>
360+
<div className="flex gap-2">
361+
{[1.0, 0.5].map((j) => (
362+
<button
363+
key={j}
364+
onClick={() => setCreateForm((f) => ({ ...f, juz_per_night: j }))}
365+
className={`flex-1 py-2 rounded-lg border text-sm font-medium transition-colors ${
366+
createForm.juz_per_night === j
367+
? "border-mosque-gold bg-mosque-gold/10 text-mosque-gold"
368+
: "border-gray-700 text-gray-400"
369+
}`}
370+
>
371+
{j === 1.0 ? "Full Juz" : "Half Juz"}
372+
</button>
373+
))}
374+
</div>
375+
</div>
333376
</div>
334-
</div>
335377

336-
<div>
337-
<label className="text-xs text-gray-400 mb-1 block">Juz</label>
338-
<select
339-
value={createForm.juz_number}
340-
onChange={(e) => setCreateForm((f) => ({ ...f, juz_number: Number(e.target.value) }))}
341-
className="w-full bg-mosque-darkest border border-white/10 rounded-lg px-3 py-2 text-white text-sm outline-none focus:border-mosque-gold/50"
378+
<button
379+
onClick={async () => {
380+
setCreating(true);
381+
try {
382+
const res = await privateRoomsApi.create(createForm);
383+
setCreatedRoom({ id: res.data.id, room_url: res.data.room_url });
384+
// Reload private rooms list in background
385+
privateRoomsApi.list().then((r) => setPrivateRooms(r.data)).catch(() => {});
386+
// Load friends for invite step
387+
friendsApi.getAll().then((r) => setFriends(r.data.friends)).catch(() => {});
388+
} finally {
389+
setCreating(false);
390+
}
391+
}}
392+
disabled={creating}
393+
className="w-full py-3 bg-mosque-gold text-mosque-dark font-bold rounded-xl hover:bg-mosque-gold-light transition-colors disabled:opacity-50"
342394
>
343-
{Array.from({ length: 30 }, (_, i) => i + 1).map((n) => (
344-
<option key={n} value={n}>Juz {n}</option>
345-
))}
346-
</select>
347-
</div>
395+
{creating ? "Creating…" : "Create Room"}
396+
</button>
397+
</>
398+
) : (
399+
/* Step 2 — invite friends */
400+
<>
401+
<div className="flex items-center justify-between">
402+
<h2 className="font-bold text-white">Invite Friends</h2>
403+
<button
404+
onClick={() => {
405+
setShowCreateModal(false);
406+
setCreatedRoom(null);
407+
setInviteBusy({});
408+
setInviteDone({});
409+
}}
410+
className="text-gray-500 hover:text-gray-300"
411+
>
412+
413+
</button>
414+
</div>
348415

349-
<div>
350-
<label className="text-xs text-gray-400 mb-1 block">Amount</label>
351-
<div className="flex gap-2">
352-
{[1.0, 0.5].map((j) => (
353-
<button
354-
key={j}
355-
onClick={() => setCreateForm((f) => ({ ...f, juz_per_night: j }))}
356-
className={`flex-1 py-2 rounded-lg border text-sm font-medium transition-colors ${
357-
createForm.juz_per_night === j
358-
? "border-mosque-gold bg-mosque-gold/10 text-mosque-gold"
359-
: "border-gray-700 text-gray-400"
360-
}`}
361-
>
362-
{j === 1.0 ? "Full Juz" : "Half Juz"}
363-
</button>
364-
))}
416+
{/* Share link */}
417+
<div className="bg-mosque-darkest border border-white/10 rounded-lg px-3 py-2">
418+
<p className="text-xs text-gray-400 mb-1">Room link</p>
419+
<p className="text-xs text-mosque-gold break-all">{createdRoom.room_url}</p>
365420
</div>
366-
</div>
367-
</div>
368421

369-
<button
370-
onClick={async () => {
371-
setCreating(true);
372-
try {
373-
await privateRoomsApi.create(createForm);
374-
const res = await privateRoomsApi.list();
375-
setPrivateRooms(res.data);
376-
setShowCreateModal(false);
377-
} finally {
378-
setCreating(false);
379-
}
380-
}}
381-
disabled={creating}
382-
className="w-full py-3 bg-mosque-gold text-mosque-dark font-bold rounded-xl hover:bg-mosque-gold-light transition-colors disabled:opacity-50"
383-
>
384-
{creating ? "Creating…" : "Create Room"}
385-
</button>
422+
{/* Friends list */}
423+
{friends.length === 0 ? (
424+
<p className="text-sm text-gray-500 text-center py-2">
425+
No friends yet.{" "}
426+
<Link href="/friends" className="text-mosque-gold underline" onClick={() => { setShowCreateModal(false); setCreatedRoom(null); }}>
427+
Add friends
428+
</Link>
429+
</p>
430+
) : (
431+
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
432+
{friends.map((f) => (
433+
<div key={f.id} className="flex items-center justify-between">
434+
<div>
435+
<p className="text-sm text-white">{f.name || f.email}</p>
436+
{f.name && <p className="text-xs text-gray-500">{f.email}</p>}
437+
</div>
438+
<button
439+
disabled={inviteBusy[f.id] || inviteDone[f.id]}
440+
onClick={async () => {
441+
setInviteBusy((p) => ({ ...p, [f.id]: true }));
442+
try {
443+
await privateRoomsApi.invite(createdRoom.id, f.id);
444+
setInviteDone((p) => ({ ...p, [f.id]: true }));
445+
} finally {
446+
setInviteBusy((p) => ({ ...p, [f.id]: false }));
447+
}
448+
}}
449+
className={`text-xs px-3 py-1.5 rounded-lg font-medium transition-colors ${
450+
inviteDone[f.id]
451+
? "bg-green-900/40 text-green-400 cursor-default"
452+
: "bg-mosque-gold/10 border border-mosque-gold/30 text-mosque-gold hover:bg-mosque-gold/20 disabled:opacity-50"
453+
}`}
454+
>
455+
{inviteDone[f.id] ? "Invited" : inviteBusy[f.id] ? "…" : "Invite"}
456+
</button>
457+
</div>
458+
))}
459+
</div>
460+
)}
461+
462+
<Link
463+
href={`/room/${createdRoom.id}`}
464+
className="block w-full py-3 bg-mosque-gold text-mosque-dark font-bold rounded-xl hover:bg-mosque-gold-light transition-colors text-center"
465+
onClick={() => { setShowCreateModal(false); setCreatedRoom(null); }}
466+
>
467+
Go to Room
468+
</Link>
469+
</>
470+
)}
386471
</div>
387472
</div>
388473
)}

0 commit comments

Comments
 (0)