From 79c82f6e65fac489dea09c2647ff6c8b150558ea Mon Sep 17 00:00:00 2001 From: Ishta P Jain Date: Sat, 23 May 2026 23:17:53 +0530 Subject: [PATCH 1/3] :wqfeat: add repository-based collaboration rooms with GitHub user invites (#459) :wq qa! wq: :wq :wq :wq :wa :wq --- package-lock.json | 23 ++- src/app/api/rooms/[roomId]/RoomClient.tsx | 79 +++++++++++ src/app/api/rooms/[roomId]/invite/route.ts | 37 +++++ src/app/api/rooms/[roomId]/messages/route.ts | 42 ++++++ src/app/api/rooms/[roomId]/page.tsx | 29 ++++ src/app/api/rooms/[roomId]/route.ts | 40 ++++++ src/app/api/rooms/route.ts | 32 +++++ src/app/dashboard/page.tsx | 6 + src/app/rooms/RoomsListClient.tsx | 80 +++++++++++ src/app/rooms/[roomId]/RoomClient.tsx | 103 ++++++++++++++ src/app/rooms/[roomId]/page.tsx | 29 ++++ src/app/rooms/page.tsx | 27 ++++ src/components/rooms/CreateRoomModal.tsx | 126 +++++++++++++++++ src/components/rooms/InviteModal.tsx | 76 ++++++++++ src/components/rooms/MembersPanel.tsx | 64 +++++++++ src/components/rooms/MessageFeed.tsx | 103 ++++++++++++++ src/components/rooms/MessageInput.tsx | 66 +++++++++ src/lib/supabase.ts | 141 +++++++++++++++++-- src/types/rooms.ts | 49 +++++++ supabase/schema.sql | 73 ++++++++++ 20 files changed, 1211 insertions(+), 14 deletions(-) create mode 100644 src/app/api/rooms/[roomId]/RoomClient.tsx create mode 100644 src/app/api/rooms/[roomId]/invite/route.ts create mode 100644 src/app/api/rooms/[roomId]/messages/route.ts create mode 100644 src/app/api/rooms/[roomId]/page.tsx create mode 100644 src/app/api/rooms/[roomId]/route.ts create mode 100644 src/app/api/rooms/route.ts create mode 100644 src/app/rooms/RoomsListClient.tsx create mode 100644 src/app/rooms/[roomId]/RoomClient.tsx create mode 100644 src/app/rooms/[roomId]/page.tsx create mode 100644 src/app/rooms/page.tsx create mode 100644 src/components/rooms/CreateRoomModal.tsx create mode 100644 src/components/rooms/InviteModal.tsx create mode 100644 src/components/rooms/MembersPanel.tsx create mode 100644 src/components/rooms/MessageFeed.tsx create mode 100644 src/components/rooms/MessageInput.tsx create mode 100644 src/types/rooms.ts diff --git a/package-lock.json b/package-lock.json index fb3967b9..210757a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -953,7 +953,8 @@ "version": "1.49.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", - "dev": true, + "devOptional": true, + "peer": true, "dependencies": { "playwright": "1.49.1" }, @@ -1535,6 +1536,7 @@ "integrity": "sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1565,6 +1567,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2265,6 +2268,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2740,6 +2744,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3720,6 +3725,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3888,6 +3894,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5420,6 +5427,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -5504,6 +5512,7 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", @@ -5867,6 +5876,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", @@ -6478,7 +6488,7 @@ "version": "1.49.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", - "dev": true, + "devOptional": true, "dependencies": { "playwright-core": "1.49.1" }, @@ -6496,7 +6506,7 @@ "version": "1.49.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", - "dev": true, + "devOptional": true, "bin": { "playwright-core": "cli.js" }, @@ -6548,6 +6558,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6718,6 +6729,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -6808,6 +6820,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6820,6 +6833,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7861,6 +7875,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -8019,6 +8034,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8224,6 +8240,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/api/rooms/[roomId]/RoomClient.tsx b/src/app/api/rooms/[roomId]/RoomClient.tsx new file mode 100644 index 00000000..c4c76463 --- /dev/null +++ b/src/app/api/rooms/[roomId]/RoomClient.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import type { CollaborationRoom, RoomMember, RoomMessage } from '@/types/rooms'; +import MessageFeed from '@/components/rooms/MessageFeed'; +import MessageInput from '@/components/rooms/MessageInput'; +import MembersPanel from '@/components/rooms/MembersPanel'; + +interface Props { + room: CollaborationRoom & { is_owner: boolean }; + initialMembers: RoomMember[]; + initialMessages: RoomMessage[]; + currentUser: string; + currentUserAvatar: string | null; +} + +export default function RoomClient({ + room, initialMembers, initialMessages, currentUser, +}: Props) { + const [messages, setMessages] = useState(initialMessages); + const [members, setMembers] = useState(initialMembers); + + function handleSent(msg: RoomMessage) { + setMessages((prev) => [...prev, msg]); + } + + function handleMemberAdded(username: string) { + setMembers((prev) => [ + ...prev, + { + id: crypto.randomUUID(), + room_id: room.id, + github_username: username, + role: 'member', + joined_at: new Date().toISOString(), + }, + ]); + } + + return ( +
+
+ + ← Rooms + +
+ +
+
+ + +
+ +
+
+ ); +} diff --git a/src/app/api/rooms/[roomId]/invite/route.ts b/src/app/api/rooms/[roomId]/invite/route.ts new file mode 100644 index 00000000..131b078a --- /dev/null +++ b/src/app/api/rooms/[roomId]/invite/route.ts @@ -0,0 +1,37 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { getRoomById, getRoomMembers, addRoomMember } from '@/lib/supabase'; +import { NextResponse } from 'next/server'; + +export async function POST( + req: Request, + { params }: { params: { roomId: string } } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const room = await getRoomById(params.roomId, session.user.name); + if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + if (!room.is_owner) + return NextResponse.json({ error: 'Only the room owner can invite' }, { status: 403 }); + const { github_username } = await req.json(); + if (!github_username?.trim()) + return NextResponse.json({ error: 'github_username required' }, { status: 400 }); + const ghRes = await fetch(`https://api.github.com/users/${github_username}`, { + headers: { + Accept: 'application/vnd.github+json', + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }, + }); + if (ghRes.status === 404) + return NextResponse.json({ error: `GitHub user "${github_username}" does not exist` }, { status: 404 }); + if (!ghRes.ok) + return NextResponse.json({ error: 'Could not verify GitHub user' }, { status: 502 }); + const members = await getRoomMembers(params.roomId); + if (members.some((m) => m.github_username === github_username)) + return NextResponse.json({ error: 'User is already a member' }, { status: 409 }); + await addRoomMember(params.roomId, github_username); + return NextResponse.json({ success: true }); +} \ No newline at end of file diff --git a/src/app/api/rooms/[roomId]/messages/route.ts b/src/app/api/rooms/[roomId]/messages/route.ts new file mode 100644 index 00000000..91a91202 --- /dev/null +++ b/src/app/api/rooms/[roomId]/messages/route.ts @@ -0,0 +1,42 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { getRoomById, getRoomMessages, sendRoomMessage } from '@/lib/supabase'; +import { NextResponse } from 'next/server'; + +export async function GET( + req: Request, + { params }: { params: { roomId: string } } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const room = await getRoomById(params.roomId, session.user.name); + if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + const url = new URL(req.url); + const before = url.searchParams.get('before') ?? undefined; + const messages = await getRoomMessages(params.roomId, 50, before); + return NextResponse.json(messages); +} + +export async function POST( + req: Request, + { params }: { params: { roomId: string } } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const room = await getRoomById(params.roomId, session.user.name); + if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + const { content } = await req.json(); + if (!content?.trim()) + return NextResponse.json({ error: 'Content required' }, { status: 400 }); + if (content.length > 4000) + return NextResponse.json({ error: 'Message too long' }, { status: 400 }); + const message = await sendRoomMessage( + params.roomId, + session.user.name, + session.user.image ?? null, + content.trim() + ); + return NextResponse.json(message, { status: 201 }); +} \ No newline at end of file diff --git a/src/app/api/rooms/[roomId]/page.tsx b/src/app/api/rooms/[roomId]/page.tsx new file mode 100644 index 00000000..c8442b57 --- /dev/null +++ b/src/app/api/rooms/[roomId]/page.tsx @@ -0,0 +1,29 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { redirect, notFound } from 'next/navigation'; +import { getRoomById, getRoomMembers, getRoomMessages } from '@/lib/supabase'; +import RoomClient from './RoomClient'; + +interface Props { + params: { roomId: string }; +} + +export default async function RoomPage({ params }: Props) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) redirect('/api/auth/signin'); + const [room, members, messages] = await Promise.all([ + getRoomById(params.roomId, session.user.name), + getRoomMembers(params.roomId), + getRoomMessages(params.roomId, 50), + ]); + if (!room) notFound(); + return ( + + ); +} \ No newline at end of file diff --git a/src/app/api/rooms/[roomId]/route.ts b/src/app/api/rooms/[roomId]/route.ts new file mode 100644 index 00000000..a6455ca4 --- /dev/null +++ b/src/app/api/rooms/[roomId]/route.ts @@ -0,0 +1,40 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { getRoomById, getRoomMembers } from '@/lib/supabase'; +import { supabaseAdmin } from '@/lib/supabase'; +import { NextResponse } from 'next/server'; + +export async function GET( + _req: Request, + { params }: { params: { roomId: string } } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const room = await getRoomById(params.roomId, session.user.name); + if (!room) return NextResponse.json({ error: 'Not found or not a member' }, { status: 404 }); + const members = await getRoomMembers(params.roomId); + return NextResponse.json({ ...room, members }); +} + +export async function DELETE( + _req: Request, + { params }: { params: { roomId: string } } +) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const room = await getRoomById(params.roomId, session.user.name); + if (!room) return NextResponse.json({ error: 'Not found' }, { status: 404 }); + if (!room.is_owner) + return NextResponse.json({ error: 'Only the owner can delete this room' }, { status: 403 }); + + const { error } = await supabaseAdmin + .from('collaboration_rooms') + .delete() + .eq('id', params.roomId); + + if (error) return NextResponse.json({ error: error.message }, { status: 500 }); + return NextResponse.json({ success: true }); +} \ No newline at end of file diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts new file mode 100644 index 00000000..7c6503be --- /dev/null +++ b/src/app/api/rooms/route.ts @@ -0,0 +1,32 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { createRoom, getRoomsForUser } from '@/lib/supabase'; +import { NextResponse } from 'next/server'; +import type { CreateRoomPayload } from '@/types/rooms'; + +export async function GET() { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + try { + const rooms = await getRoomsForUser(session.user.name); + return NextResponse.json(rooms); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} + +export async function POST(req: Request) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const body: CreateRoomPayload = await req.json(); + if (!body.name?.trim() || !body.repo_owner?.trim() || !body.repo_name?.trim()) + return NextResponse.json({ error: 'name, repo_owner, and repo_name are required' }, { status: 400 }); + try { + const room = await createRoom(body, session.user.name); + return NextResponse.json(room, { status: 201 }); + } catch (err: any) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index e0de40ee..2dc7520a 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -44,6 +44,12 @@ export default async function DashboardPage() { > Settings + + Rooms + diff --git a/src/app/rooms/RoomsListClient.tsx b/src/app/rooms/RoomsListClient.tsx new file mode 100644 index 00000000..ddb39dda --- /dev/null +++ b/src/app/rooms/RoomsListClient.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import type { CollaborationRoom } from '@/types/rooms'; +import CreateRoomModal from '@/components/rooms/CreateRoomModal'; + +interface Props { + initialRooms: CollaborationRoom[]; + currentUser: string; +} + +export default function RoomsListClient({ initialRooms, currentUser }: Props) { + const [rooms, setRooms] = useState(initialRooms); + const [showCreate, setShowCreate] = useState(false); + + return ( +
+ {/* Header */} +
+
+

Collaboration Rooms

+

+ Repository-linked discussion spaces for your team +

+
+ +
+ + {/* Room cards */} + {rooms.length === 0 ? ( +
+

No rooms yet

+

Create your first collaboration room to get started.

+
+ ) : ( +
+ {rooms.map((room) => ( + +
+
+

{room.name}

+ {room.description && ( +

{room.description}

+ )} +
+ + {room.repo_owner}/{room.repo_name} + +
+
+ + {room.is_owner ? 'πŸ‘‘ Owner' : 'πŸ‘€ Member'} + + Β· + Created {new Date(room.created_at).toLocaleDateString('en-US')} +
+ + ))} +
+ )} + + {showCreate && ( + setShowCreate(false)} + onCreated={(room) => setRooms((prev) => [room, ...prev])} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/src/app/rooms/[roomId]/RoomClient.tsx b/src/app/rooms/[roomId]/RoomClient.tsx new file mode 100644 index 00000000..6d00aa88 --- /dev/null +++ b/src/app/rooms/[roomId]/RoomClient.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import type { CollaborationRoom, RoomMember, RoomMessage } from '@/types/rooms'; +import MessageFeed from '@/components/rooms/MessageFeed'; +import MessageInput from '@/components/rooms/MessageInput'; +import MembersPanel from '@/components/rooms/MembersPanel'; + +interface Props { + room: CollaborationRoom & { is_owner: boolean }; + initialMembers: RoomMember[]; + initialMessages: RoomMessage[]; + currentUser: string; + currentUserAvatar: string | null; +} + +export default function RoomClient({ + room, initialMembers, initialMessages, currentUser, +}: Props) { + const router = useRouter(); + const [messages, setMessages] = useState(initialMessages); + const [members, setMembers] = useState(initialMembers); + + function handleSent(msg: RoomMessage) { + setMessages((prev) => [...prev, msg]); + } + + function handleMemberAdded(username: string) { + setMembers((prev) => [ + ...prev, + { + id: crypto.randomUUID(), + room_id: room.id, + github_username: username, + role: 'member', + joined_at: new Date().toISOString(), + }, + ]); + } + + async function handleDeleteRoom() { + if (!confirm('Are you sure you want to delete this room? This cannot be undone.')) return; + const res = await fetch(`/api/rooms/${room.id}`, { method: 'DELETE' }); + if (res.ok) { + router.push('/rooms'); + } else { + const data = await res.json(); + alert(data.error ?? 'Failed to delete room'); + } + } + + return ( +
+
+
+ + ← Rooms + + + + {room.is_owner && ( + + )} +
+ +
+
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/app/rooms/[roomId]/page.tsx b/src/app/rooms/[roomId]/page.tsx new file mode 100644 index 00000000..c8442b57 --- /dev/null +++ b/src/app/rooms/[roomId]/page.tsx @@ -0,0 +1,29 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { redirect, notFound } from 'next/navigation'; +import { getRoomById, getRoomMembers, getRoomMessages } from '@/lib/supabase'; +import RoomClient from './RoomClient'; + +interface Props { + params: { roomId: string }; +} + +export default async function RoomPage({ params }: Props) { + const session = await getServerSession(authOptions); + if (!session?.user?.name) redirect('/api/auth/signin'); + const [room, members, messages] = await Promise.all([ + getRoomById(params.roomId, session.user.name), + getRoomMembers(params.roomId), + getRoomMessages(params.roomId, 50), + ]); + if (!room) notFound(); + return ( + + ); +} \ No newline at end of file diff --git a/src/app/rooms/page.tsx b/src/app/rooms/page.tsx new file mode 100644 index 00000000..cdb345f8 --- /dev/null +++ b/src/app/rooms/page.tsx @@ -0,0 +1,27 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { redirect } from 'next/navigation'; +import { getRoomsForUser } from '@/lib/supabase'; +import RoomsListClient from './RoomsListClient'; +import Link from 'next/link'; + +export const metadata = { title: 'Collaboration Rooms β€” DevTrack' }; + +export default async function RoomsPage() { + const session = await getServerSession(authOptions); + if (!session?.user?.name) redirect('/api/auth/signin'); + const rooms = await getRoomsForUser(session.user.name); + return ( +
+
+ + ← Back to Dashboard + +
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/rooms/CreateRoomModal.tsx b/src/components/rooms/CreateRoomModal.tsx new file mode 100644 index 00000000..23607a82 --- /dev/null +++ b/src/components/rooms/CreateRoomModal.tsx @@ -0,0 +1,126 @@ +/// + +'use client'; + +import React, { useState, type FormEvent } from 'react'; +import type { CollaborationRoom, CreateRoomPayload } from '@/types/rooms'; + +interface Props { + onClose: () => void; + onCreated: (room: CollaborationRoom) => void; +} + +export default function CreateRoomModal({ onClose, onCreated }: Props) { + const [form, setForm] = useState({ + name: '', + description: '', + repo_owner: '', + repo_name: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const res = await fetch('/api/rooms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data?.error ?? 'Failed to create room'); + return; + } + + onCreated(data); + onClose(); + } catch (err) { + setError('Failed to create room'); + } finally { + setLoading(false); + } + } + + return ( + <> +
+
+

Create Collaboration Room

+ +
+
+ + setForm({ ...form, name: e.target.value })} + required + /> +
+ +
+ + setForm({ ...form, repo_owner: e.target.value })} + required + /> +
+ +
+ + setForm({ ...form, repo_name: e.target.value })} + required + /> +
+ +
+ +