mirror of
https://github.com/better-auth/better-hub
synced 2026-08-09 12:26:04 +00:00
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:
@@ -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={
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user