Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions src/app/api/rooms/[roomId]/RoomClient.tsx
Original file line number Diff line number Diff line change
@@ -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<RoomMessage[]>(initialMessages);
const [members, setMembers] = useState<RoomMember[]>(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 (
<div className="flex flex-col h-screen bg-[var(--background)] text-[var(--foreground)]">
<header className="border-b border-[var(--border)] px-4 py-3 flex items-center gap-3 shrink-0">
<Link href="/rooms" className="text-sm text-gray-400 hover:text-gray-600">
← Rooms
</Link>
<div className="h-4 w-px bg-gray-200 dark:bg-gray-700" />
<div>
<h1 className="font-semibold text-base leading-tight">{room.name}</h1>

<a href={`https://github.com/${room.repo_owner}/${room.repo_name}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-500 hover:underline"
>
{room.repo_owner}/{room.repo_name}
</a>
</div>
</header>

<div className="flex flex-1 overflow-hidden">
<div className="flex flex-col flex-1 overflow-hidden">
<MessageFeed
roomId={room.id}
currentUser={currentUser}
initialMessages={messages}
/>
<MessageInput roomId={room.id} onSent={handleSent} />
</div>
<MembersPanel
roomId={room.id}
members={members}
isOwner={room.is_owner}
onMemberAdded={handleMemberAdded}
/>
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions src/app/api/rooms/[roomId]/invite/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
42 changes: 42 additions & 0 deletions src/app/api/rooms/[roomId]/messages/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
29 changes: 29 additions & 0 deletions src/app/api/rooms/[roomId]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<RoomClient
room={room}
initialMembers={members}
initialMessages={messages}
currentUser={session.user.name}
currentUserAvatar={session.user.image ?? null}
/>
);
}
40 changes: 40 additions & 0 deletions src/app/api/rooms/[roomId]/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
32 changes: 32 additions & 0 deletions src/app/api/rooms/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Loading
Loading