feat: clickable username in prompt tab's messages (#114)

## Warning!
This PR requires migration.
```
cd apps/web
pnpx prisma generate
pnpx prisma db push
```

---

Allows you to redirect to the user profile:

<img width="2044" height="1140" alt="CleanShot 2026-02-25 at 23 06
01@2x"
src="https://github.com/user-attachments/assets/71acc15f-622d-4869-bd82-d5b82b70068f"
/>

Also fixes inconsistency with the UI counter for closed prompts

<img width="2588" height="1166" alt="Attached_image"
src="https://github.com/user-attachments/assets/ecaf478b-da72-4ce5-b22b-a896400c504e"
/>

Also adds the author of a prompt to the UI:

<img width="2618" height="1286" alt="CleanShot 2026-02-25 at 23 25
59@2x"
src="https://github.com/user-attachments/assets/2cdc3e39-abe8-4377-9622-76c065991144"
/>
This commit is contained in:
Maxwell
2026-02-25 23:36:26 -08:00
committed by GitHub
parent 8ff1c11e70
commit d23eb9af90
6 changed files with 152 additions and 15 deletions
+4
View File
@@ -238,6 +238,9 @@ model PinnedItem {
model PromptRequest {
id String @id
userId String
userLogin String?
userName String?
userAvatarUrl String?
owner String
repo String
title String
@@ -257,6 +260,7 @@ model PromptRequestComment {
id String @id
promptRequestId String
userId String
userLogin String?
userName String
userAvatarUrl String
body String
@@ -2,8 +2,7 @@ import type { Metadata } from "next";
import { getPromptRequest, listPromptRequestComments } from "@/lib/prompt-request-store";
import { PromptDetail } from "@/components/prompt-request/prompt-detail";
import { notFound } from "next/navigation";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { getServerSession } from "@/lib/auth";
import { getOctokit, extractRepoPermissions } from "@/lib/github";
export async function generateMetadata({
@@ -28,7 +27,7 @@ export default async function PromptDetailPage({
const [promptRequest, comments, session] = await Promise.all([
getPromptRequest(id),
listPromptRequestComments(id),
auth.api.getSession({ headers: await headers() }),
getServerSession(),
]);
if (!promptRequest) {
@@ -36,7 +35,12 @@ export default async function PromptDetailPage({
}
const currentUser = session?.user
? { id: session.user.id, name: session.user.name, image: session.user.image ?? "" }
? {
id: session.user.id,
login: session.githubUser?.login ?? null,
name: session.user.name,
image: session.user.image ?? "",
}
: null;
// Check repo maintainer permissions (push/admin/maintain)
@@ -11,7 +11,7 @@ import {
deletePromptRequestComment,
getPromptRequestComment,
} from "@/lib/prompt-request-store";
import { auth } from "@/lib/auth";
import { auth, getServerSession } from "@/lib/auth";
import { headers } from "next/headers";
import { getOctokit, extractRepoPermissions } from "@/lib/github";
@@ -64,7 +64,7 @@ export async function acceptPromptRequestAction(id: string) {
}
export async function createPromptRequestAction(owner: string, repo: string, body: string) {
const session = await auth.api.getSession({ headers: await headers() });
const session = await getServerSession();
if (!session?.user?.id) throw new Error("Unauthorized");
// Auto-generate title from first line of body
@@ -75,7 +75,16 @@ export async function createPromptRequestAction(owner: string, repo: string, bod
.trim() || "Untitled prompt";
const title = firstLine.length > 80 ? firstLine.slice(0, 77) + "..." : firstLine;
const pr = await createPromptRequest(session.user.id, owner, repo, title, body);
const pr = await createPromptRequest(
session.user.id,
session.githubUser?.login ?? null,
session.user.name,
session.user.image ?? null,
owner,
repo,
title,
body,
);
revalidatePath(`/repos/${owner}/${repo}/prompts`);
return pr;
}
@@ -114,7 +123,7 @@ export async function deletePromptRequestAction(id: string) {
}
export async function addPromptComment(promptRequestId: string, body: string) {
const session = await auth.api.getSession({ headers: await headers() });
const session = await getServerSession();
if (!session?.user?.id) throw new Error("Unauthorized");
const pr = await getPromptRequest(promptRequestId);
@@ -123,6 +132,7 @@ export async function addPromptComment(promptRequestId: string, body: string) {
const comment = await createPromptRequestComment(
promptRequestId,
session.user.id,
session.githubUser?.login ?? null,
session.user.name,
session.user.image ?? "",
body,
@@ -54,7 +54,7 @@ interface PromptDetailProps {
repo: string;
promptRequest: PromptRequest;
comments: PromptRequestComment[];
currentUser: { id: string; name: string; image: string } | null;
currentUser: { id: string; login: string | null; name: string; image: string } | null;
canManage: boolean;
isMaintainer: boolean;
}
@@ -101,6 +101,7 @@ export function PromptDetail({
id: `optimistic-${Date.now()}`,
promptRequestId: promptRequest.id,
userId: currentUser.id,
userLogin: currentUser.login,
userName: currentUser.name,
userAvatarUrl: currentUser.image,
body,
@@ -437,11 +438,22 @@ export function PromptDetail({
) : (
<div className="w-[18px] h-[18px] rounded-full bg-muted" />
)}
<span className="text-[11px] font-medium text-foreground">
{
comment.userName
}
</span>
{comment.userLogin ? (
<Link
href={`/users/${comment.userLogin}`}
className="text-[11px] font-medium text-foreground hover:underline"
>
{
comment.userName
}
</Link>
) : (
<span className="text-[11px] font-medium text-foreground">
{
comment.userName
}
</span>
)}
<span className="text-[10px] text-muted-foreground/40 font-mono">
<TimeAgo
date={
@@ -527,6 +539,47 @@ export function PromptDetail({
{/* Right — Metadata sidebar */}
<div className="hidden md:block w-56 shrink-0 space-y-4">
{/* Author */}
<div className="space-y-1.5">
<p className="text-[10px] font-mono text-muted-foreground/40 uppercase tracking-wider">
Author
</p>
<div className="flex items-center gap-2">
{promptRequest.userAvatarUrl ? (
<Image
src={
promptRequest.userAvatarUrl
}
alt={
promptRequest.userName ??
"User"
}
width={20}
height={20}
className="rounded-full"
/>
) : (
<div className="w-5 h-5 rounded-full bg-muted" />
)}
{promptRequest.userLogin ? (
<Link
href={`/users/${promptRequest.userLogin}`}
className="text-[11px] font-medium text-foreground hover:underline"
>
{promptRequest.userName ??
promptRequest.userLogin}
</Link>
) : (
<span className="text-[11px] font-medium text-muted-foreground/60">
{promptRequest.userName ??
"Unknown"}
</span>
)}
</div>
</div>
<div className="h-px bg-border/30" />
{/* Status */}
<div className="space-y-1.5">
<p className="text-[10px] font-mono text-muted-foreground/40 uppercase tracking-wider">
@@ -2,6 +2,7 @@
import { useState, useMemo, useTransition, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import {
Sparkles,
@@ -102,7 +103,7 @@ export function PromptList({ owner, repo, promptRequests }: PromptListProps) {
const c = { open: 0, closed: 0 };
for (const pr of promptRequests) {
if (pr.status === "open") c.open++;
else c.closed++;
else if (pr.status === "accepted" || pr.status === "closed") c.closed++;
}
return c;
}, [promptRequests]);
@@ -323,6 +324,51 @@ export function PromptList({ owner, repo, promptRequests }: PromptListProps) {
)}
</div>
<div className="flex items-center gap-2 mt-0.5">
{pr.userName && (
<>
{pr.userAvatarUrl && (
<Image
src={
pr.userAvatarUrl
}
alt={
pr.userName
}
width={
14
}
height={
14
}
className="rounded-full"
/>
)}
{pr.userLogin ? (
<Link
href={`/users/${pr.userLogin}`}
onClick={(
e,
) =>
e.stopPropagation()
}
className="text-[11px] text-muted-foreground/60 hover:text-foreground hover:underline"
>
{
pr.userName
}
</Link>
) : (
<span className="text-[11px] text-muted-foreground/60">
{
pr.userName
}
</span>
)}
<span className="text-muted-foreground/30">
·
</span>
</>
)}
<span className="text-[11px] text-muted-foreground/50 font-mono">
<TimeAgo
date={
+20
View File
@@ -5,6 +5,9 @@ export type PromptRequestStatus = "open" | "accepted" | "closed";
export interface PromptRequest {
id: string;
userId: string;
userLogin: string | null;
userName: string | null;
userAvatarUrl: string | null;
owner: string;
repo: string;
title: string;
@@ -19,6 +22,9 @@ export interface PromptRequest {
function toPromptRequest(row: {
id: string;
userId: string;
userLogin: string | null;
userName: string | null;
userAvatarUrl: string | null;
owner: string;
repo: string;
title: string;
@@ -32,6 +38,9 @@ function toPromptRequest(row: {
return {
id: row.id,
userId: row.userId,
userLogin: row.userLogin,
userName: row.userName,
userAvatarUrl: row.userAvatarUrl,
owner: row.owner,
repo: row.repo,
title: row.title,
@@ -46,6 +55,9 @@ function toPromptRequest(row: {
export async function createPromptRequest(
userId: string,
userLogin: string | null,
userName: string | null,
userAvatarUrl: string | null,
owner: string,
repo: string,
title: string,
@@ -58,6 +70,9 @@ export async function createPromptRequest(
data: {
id,
userId,
userLogin,
userName,
userAvatarUrl,
owner,
repo,
title,
@@ -153,6 +168,7 @@ export interface PromptRequestComment {
id: string;
promptRequestId: string;
userId: string;
userLogin: string | null;
userName: string;
userAvatarUrl: string;
body: string;
@@ -164,6 +180,7 @@ function toPromptRequestComment(row: {
id: string;
promptRequestId: string;
userId: string;
userLogin: string | null;
userName: string;
userAvatarUrl: string;
body: string;
@@ -174,6 +191,7 @@ function toPromptRequestComment(row: {
id: row.id,
promptRequestId: row.promptRequestId,
userId: row.userId,
userLogin: row.userLogin,
userName: row.userName,
userAvatarUrl: row.userAvatarUrl,
body: row.body,
@@ -185,6 +203,7 @@ function toPromptRequestComment(row: {
export async function createPromptRequestComment(
promptRequestId: string,
userId: string,
userLogin: string | null,
userName: string,
userAvatarUrl: string,
body: string,
@@ -197,6 +216,7 @@ export async function createPromptRequestComment(
id,
promptRequestId,
userId,
userLogin,
userName,
userAvatarUrl,
body,