Skip to content

Commit 9aff1b7

Browse files
prazgaitisclaude
andauthored
Redesign admin feedback as two-column layout and show full flag reason (#223)
Consolidate feedback detail into a side panel (removing separate route), and remove line-clamp on flagged activity reason so long text is fully visible. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2d84009 commit 9aff1b7

11 files changed

Lines changed: 76 additions & 65 deletions

File tree

.claude/napkin.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
| 2026-03-03 | self | Tried writing `apps/web/app/challenges/[id]/admin/feedback/page.tsx` before creating the nested directory and got `no such file or directory` | Create route directories first (`mkdir -p ...`) before redirecting heredoc output to a new file |
1818
| 2026-03-10 | self | Passed a dynamic-route path with `[]` and `()` unquoted into `rg`, and zsh globbing failed again while comparing mini-game profile files | Quote every individual dynamic route path in shell commands, even when mixing them with normal paths in the same invocation |
1919
| 2026-03-10 | self | Ran workspace-scoped ESLint from `apps/web` but still passed repo-root file paths, so ESLint reported no matching files | When using `pnpm -F web exec ...` from the workspace directory, pass paths relative to `apps/web` |
20+
| 2026-03-11 | self | Created a task markdown file with shell redirection instead of `apply_patch` | Use `apply_patch` for file creation/editing, including task files required by repo process |
2021
| 2026-03-01 | self | Started repo exploration before reading `.claude/napkin.md` again | Always run `cat .claude/napkin.md` as the very first command in a new session |
2122
| 2026-03-01 | self | CSV row typing was too narrow (`string | number`) and failed once boolean `isNegative` values were added | For CSV builders, include all emitted scalar types up front (`string | number | boolean`) or coerce before push |
2223
| 2026-03-01 | self | After `pnpm -F web add ...`, got `lockfile only installation` warning and later typecheck failed on missing workspace modules | Run `pnpm install` after filtered add commands when warned about lockfile-only state, then re-run checks |
@@ -156,3 +157,4 @@
156157
| 2026-03-01 | self | Wrote pagination test assuming followed activity order by logical date; query order was by insertion/descending index order | In feed tests, assert membership or deterministic ordering inputs; don't assume `loggedDate` sort unless query enforces it |
157158
| 2026-03-02 | self | When adding a new `usePaginatedQuery` for a different Convex query alongside an existing one, both must stay mounted (hooks can't be conditional in React); use `"skip"` arg on the inactive query | Always use `"skip"` sentinel for Convex paginated queries that should be inactive based on tab state |
158159
- `followingOnly` feed filters applied after pagination can return empty initial pages despite available matches later; skip empty pages server-side before returning a page.
160+
| 2026-03-11 | self | Tried to read a Next route file with unquoted brackets and zsh globbed it | Always single-quote `app/**/[param]/**` paths in shell commands |

apps/web/app/challenges/[id]/admin/feedback/[feedbackId]/page.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

apps/web/app/challenges/[id]/admin/feedback/[feedbackId]/feedback-detail-content.tsx renamed to apps/web/app/challenges/[id]/admin/feedback/feedback-detail-content.tsx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { useCallback, useEffect, useState } from "react";
44
import { useQuery, useMutation } from "@/lib/convex-auth-react";
5-
import { useRouter } from "next/navigation";
65
import { api } from "@repo/backend";
76
import type { Id } from "@repo/backend/_generated/dataModel";
87
import { formatDistanceToNow } from "date-fns";
@@ -19,7 +18,7 @@ import { Badge } from "@/components/ui/badge";
1918
import { Button } from "@/components/ui/button";
2019
import { Input } from "@/components/ui/input";
2120
import { UserAvatar } from "@/components/user-avatar";
22-
import { useFeedbackList } from "../feedback-list-context";
21+
import { useFeedbackList } from "./feedback-list-context";
2322

2423
type FeedbackType = "bug" | "question" | "idea" | "other";
2524
type FeedbackStatus = "open" | "fixed";
@@ -98,7 +97,6 @@ export function FeedbackDetailContent({
9897
challengeId,
9998
feedbackId,
10099
}: FeedbackDetailContentProps) {
101-
const router = useRouter();
102100
const [isPending, setIsPending] = useState(false);
103101

104102
const data = useQuery(api.queries.feedback.listForAdmin, {
@@ -110,7 +108,7 @@ export function FeedbackDetailContent({
110108
const updateFeedback = useMutation(api.mutations.feedback.updateByAdmin);
111109

112110
// Keyboard navigation
113-
const { items: sidebarItems } = useFeedbackList();
111+
const { items: sidebarItems, setSelectedId } = useFeedbackList();
114112

115113
const navigateToSibling = useCallback(
116114
(direction: "prev" | "next") => {
@@ -119,21 +117,17 @@ export function FeedbackDetailContent({
119117
if (currentIndex === -1) {
120118
const fallback = sidebarItems[0];
121119
if (fallback) {
122-
router.push(
123-
`/challenges/${challengeId}/admin/feedback/${fallback.id}`,
124-
);
120+
setSelectedId(fallback.id);
125121
}
126122
return;
127123
}
128124
const targetIndex =
129125
direction === "prev" ? currentIndex - 1 : currentIndex + 1;
130126
if (targetIndex < 0 || targetIndex >= sidebarItems.length) return;
131127
const target = sidebarItems[targetIndex];
132-
router.push(
133-
`/challenges/${challengeId}/admin/feedback/${target.id}`,
134-
);
128+
setSelectedId(target.id);
135129
},
136-
[sidebarItems, feedbackId, challengeId, router],
130+
[sidebarItems, feedbackId, setSelectedId],
137131
);
138132

139133
const handleStatusToggle = useCallback(async () => {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { MessageSquare } from "lucide-react";
4+
import { useFeedbackList } from "./feedback-list-context";
5+
import { FeedbackDetailContent } from "./feedback-detail-content";
6+
7+
interface FeedbackDetailPanelProps {
8+
challengeId: string;
9+
}
10+
11+
export function FeedbackDetailPanel({ challengeId }: FeedbackDetailPanelProps) {
12+
const { selectedId } = useFeedbackList();
13+
14+
if (!selectedId) {
15+
return (
16+
<div className="flex h-full flex-col items-center justify-center text-center">
17+
<MessageSquare className="h-10 w-10 text-zinc-700 mb-3" />
18+
<p className="text-sm font-medium text-zinc-400">
19+
Select a feedback item to review
20+
</p>
21+
<p className="mt-1 text-xs text-zinc-600">
22+
Choose an item from the list to see its details and respond.
23+
</p>
24+
</div>
25+
);
26+
}
27+
28+
return (
29+
<FeedbackDetailContent
30+
key={selectedId}
31+
challengeId={challengeId}
32+
feedbackId={selectedId}
33+
/>
34+
);
35+
}

apps/web/app/challenges/[id]/admin/feedback/feedback-keyboard-shortcut-bar.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useParams } from "next/navigation";
3+
import { useFeedbackList } from "./feedback-list-context";
44

55
function Kbd({ children }: { children: React.ReactNode }) {
66
return (
@@ -26,10 +26,9 @@ function Shortcut({
2626
}
2727

2828
export function FeedbackKeyboardShortcutBar() {
29-
const params = useParams();
30-
const hasSelection = !!params.feedbackId;
29+
const { selectedId } = useFeedbackList();
3130

32-
if (!hasSelection) return null;
31+
if (!selectedId) return null;
3332

3433
return (
3534
<div className="flex flex-shrink-0 items-center gap-4 border-b border-zinc-800 bg-zinc-900/50 px-4 py-1.5 text-[10px]">

apps/web/app/challenges/[id]/admin/feedback/feedback-list-context.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,24 @@ type FeedbackItem = {
99
interface FeedbackListContextValue {
1010
items: FeedbackItem[];
1111
setItems: (items: FeedbackItem[]) => void;
12+
selectedId: string | null;
13+
setSelectedId: (id: string | null) => void;
1214
}
1315

1416
const FeedbackListContext = createContext<FeedbackListContextValue>({
1517
items: [],
1618
setItems: () => {},
19+
selectedId: null,
20+
setSelectedId: () => {},
1721
});
1822

1923
export function FeedbackListProvider({ children }: { children: ReactNode }) {
2024
const [items, setItems] = useState<FeedbackItem[]>([]);
25+
const [selectedId, setSelectedId] = useState<string | null>(null);
2126
return (
22-
<FeedbackListContext.Provider value={{ items, setItems }}>
27+
<FeedbackListContext.Provider
28+
value={{ items, setItems, selectedId, setSelectedId }}
29+
>
2330
{children}
2431
</FeedbackListContext.Provider>
2532
);

apps/web/app/challenges/[id]/admin/feedback/feedback-sidebar.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import { useQuery } from "@/lib/convex-auth-react";
44
import { api } from "@repo/backend";
55
import type { Id } from "@repo/backend/_generated/dataModel";
6-
import Link from "next/link";
7-
import { useParams } from "next/navigation";
86
import { useEffect, useState } from "react";
97
import { formatDistanceToNow } from "date-fns";
108
import { Loader2, MessageSquare, Search } from "lucide-react";
@@ -43,8 +41,7 @@ interface FeedbackSidebarProps {
4341
}
4442

4543
export function FeedbackSidebar({ challengeId }: FeedbackSidebarProps) {
46-
const params = useParams();
47-
const selectedFeedbackId = params.feedbackId as string | undefined;
44+
const { selectedId, setSelectedId, setItems } = useFeedbackList();
4845
const [search, setSearch] = useState("");
4946
const [statusFilter, setStatusFilter] = useState<"all" | "open" | "fixed">(
5047
"open",
@@ -75,11 +72,17 @@ export function FeedbackSidebar({ challengeId }: FeedbackSidebarProps) {
7572
const total = items.length;
7673

7774
// Sync visible items to context for keyboard navigation
78-
const { setItems } = useFeedbackList();
7975
useEffect(() => {
8076
setItems(items.map((item) => ({ id: item.id })));
8177
}, [items, setItems]);
8278

79+
// Auto-select first item when none is selected
80+
useEffect(() => {
81+
if (!selectedId && items.length > 0) {
82+
setSelectedId(items[0].id);
83+
}
84+
}, [selectedId, items, setSelectedId]);
85+
8386
return (
8487
<div className="flex h-full flex-col">
8588
{/* Header */}
@@ -131,7 +134,7 @@ export function FeedbackSidebar({ challengeId }: FeedbackSidebarProps) {
131134
</div>
132135

133136
{/* List */}
134-
<div className="flex-1 overflow-y-auto">
137+
<div className="flex-1 overflow-y-auto scrollbar-hide">
135138
{data === undefined ? (
136139
<div className="flex items-center justify-center py-10">
137140
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
@@ -143,12 +146,13 @@ export function FeedbackSidebar({ challengeId }: FeedbackSidebarProps) {
143146
) : (
144147
<div>
145148
{items.map((item) => {
146-
const isSelected = selectedFeedbackId === item.id;
149+
const isSelected = selectedId === item.id;
147150
return (
148-
<Link
151+
<button
149152
key={item.id}
150-
href={`/challenges/${challengeId}/admin/feedback/${item.id}`}
151-
className={`flex gap-3 border-b border-zinc-800/50 px-3 py-2.5 transition-colors ${
153+
type="button"
154+
onClick={() => setSelectedId(item.id)}
155+
className={`flex w-full text-left gap-3 border-b border-zinc-800/50 px-3 py-2.5 transition-colors ${
152156
isSelected
153157
? "bg-zinc-800 border-l-2 border-l-indigo-500"
154158
: "hover:bg-zinc-900 border-l-2 border-l-transparent"
@@ -205,7 +209,7 @@ export function FeedbackSidebar({ challengeId }: FeedbackSidebarProps) {
205209
{item.title ?? item.description}
206210
</p>
207211
</div>
208-
</Link>
212+
</button>
209213
);
210214
})}
211215
</div>

apps/web/app/challenges/[id]/admin/feedback/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
22
import { FeedbackSidebar } from "./feedback-sidebar";
33
import { FeedbackKeyboardShortcutBar } from "./feedback-keyboard-shortcut-bar";
44
import { FeedbackListProvider } from "./feedback-list-context";
5+
import { FeedbackDetailPanel } from "./feedback-detail-panel";
56

67
interface FeedbackLayoutProps {
78
children: ReactNode;
@@ -16,15 +17,17 @@ export default async function FeedbackLayout({
1617

1718
return (
1819
<FeedbackListProvider>
19-
<div className="flex -m-3 h-[calc(100dvh-6.5rem)] overflow-hidden">
20+
<div className="flex -m-3 h-full overflow-hidden">
2021
{/* Left panel — feedback list */}
2122
<div className="w-80 flex-shrink-0 border-r border-zinc-800 overflow-y-auto">
2223
<FeedbackSidebar challengeId={challengeId} />
2324
</div>
2425
{/* Right panel — hint bar + detail */}
2526
<div className="flex flex-1 flex-col overflow-hidden">
2627
<FeedbackKeyboardShortcutBar />
27-
<div className="flex-1 overflow-y-auto p-4">{children}</div>
28+
<div className="flex-1 overflow-y-auto scrollbar-hide p-4">
29+
<FeedbackDetailPanel challengeId={challengeId} />
30+
</div>
2831
</div>
2932
</div>
3033
</FeedbackListProvider>
Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
1-
import { MessageSquare } from "lucide-react";
2-
31
export default function FeedbackPage() {
4-
return (
5-
<div className="flex h-full flex-col items-center justify-center text-center">
6-
<MessageSquare className="h-10 w-10 text-zinc-700 mb-3" />
7-
<p className="text-sm font-medium text-zinc-400">
8-
Select a feedback item to review
9-
</p>
10-
<p className="mt-1 text-xs text-zinc-600">
11-
Choose an item from the list to see its details and respond.
12-
</p>
13-
</div>
14-
);
2+
return null;
153
}

apps/web/app/challenges/[id]/admin/flagged-activities/[activityId]/flagged-activity-detail-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export function FlaggedActivityDetailContent({
225225
</Link>
226226
</Button>
227227
</div>
228-
<p className="mt-0.5 text-xs text-muted-foreground line-clamp-2">
228+
<p className="mt-0.5 text-xs text-muted-foreground whitespace-pre-wrap">
229229
{activity.flaggedReason ?? "No reason provided"}
230230
</p>
231231
{/* Flagger chips */}

0 commit comments

Comments
 (0)