Revamp My Badges tab: draggable badge order, scroll areas, and pending carousel
- Extract reusable SortableList/SortableItem components sharing the same @dnd-kit pattern used by the sidebar edit view (DRY) - Replace ChevronUp/ChevronDown reorder buttons with drag-and-drop on accepted badges list - Wrap accepted and created badge sections in ScrollArea (max-h 420px) - Redesign pending badges as a carousel showing one badge at a time in the notification-style BadgeContent presentation (rotating rays, 3D tilt), with left/right arrows to navigate the pending queue
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { useCallback } from 'react';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import {
|
||||
DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext, verticalListSortingStrategy, useSortable, arrayMove,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ── Generic sortable list container ──────────────────────────────────────────
|
||||
|
||||
export interface SortableListProps<T> {
|
||||
/** Items in current order. */
|
||||
items: T[];
|
||||
/** Extract a unique stable string id from each item. */
|
||||
getItemId: (item: T, index: number) => string;
|
||||
/** Called with the reordered items array after a drag completes. */
|
||||
onReorder: (items: T[]) => void;
|
||||
/** Render each item. The wrapper provides the sortable ref, transform, and drag handle. */
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
/** Additional classes on the outer container. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic drag-and-drop sortable list.
|
||||
*
|
||||
* Reuses the same DnD-kit sensor configuration and vertical sort strategy
|
||||
* used by the sidebar edit view. Wrap each child in `<SortableItem>` to
|
||||
* get the grip handle and drag styling for free.
|
||||
*/
|
||||
export function SortableList<T>({ items, getItemId, onReorder, renderItem, className }: SortableListProps<T>) {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||
useSensor(KeyboardSensor),
|
||||
);
|
||||
|
||||
const sortableIds = items.map(getItemId);
|
||||
|
||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const oldIndex = sortableIds.indexOf(active.id as string);
|
||||
const newIndex = sortableIds.indexOf(over.id as string);
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
onReorder(arrayMove(items, oldIndex, newIndex));
|
||||
}, [sortableIds, items, onReorder]);
|
||||
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
|
||||
<div className={className}>
|
||||
{items.map((item, i) => renderItem(item, i))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Generic sortable item wrapper ────────────────────────────────────────────
|
||||
|
||||
export interface SortableItemProps {
|
||||
/** Must match the id returned by `getItemId` for this item. */
|
||||
id: string;
|
||||
/** When false the grip handle is hidden and dragging is disabled. */
|
||||
enabled?: boolean;
|
||||
/** Additional classes on the wrapper div. */
|
||||
className?: string;
|
||||
/** Classes applied while the item is being dragged. */
|
||||
draggingClassName?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a single child with `useSortable` and renders a grip-vertical
|
||||
* drag handle. Shares the same visual pattern as the sidebar edit view.
|
||||
*/
|
||||
export function SortableItem({ id, enabled = true, className, draggingClassName, children }: SortableItemProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled: !enabled });
|
||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
'flex items-center transition-colors relative',
|
||||
isDragging && (draggingClassName ?? 'z-10 opacity-80 shadow-lg'),
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{enabled && (
|
||||
<button
|
||||
className="flex items-center justify-center w-8 shrink-0 cursor-grab active:cursor-grabbing text-muted-foreground hover:text-foreground transition-colors"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+157
-184
@@ -5,8 +5,8 @@ import { useSeoMeta } from "@unhead/react";
|
||||
import {
|
||||
Award,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
@@ -23,6 +23,7 @@ import { Link } from "react-router-dom";
|
||||
import { AwardBadgeDialog } from "@/components/AwardBadgeDialog";
|
||||
import { LoginArea } from "@/components/auth/LoginArea";
|
||||
import {
|
||||
BadgeContent,
|
||||
type BadgeData,
|
||||
parseBadgeDefinition,
|
||||
} from "@/components/BadgeContent";
|
||||
@@ -33,6 +34,7 @@ import { FeedEmptyState } from "@/components/FeedEmptyState";
|
||||
import { NoteCard } from "@/components/NoteCard";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { PullToRefresh } from "@/components/PullToRefresh";
|
||||
import { SortableList, SortableItem } from "@/components/SortableList";
|
||||
import { SubHeaderBar } from "@/components/SubHeaderBar";
|
||||
import { TabButton } from "@/components/TabButton";
|
||||
import {
|
||||
@@ -51,6 +53,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useLayoutOptions } from "@/contexts/LayoutContext";
|
||||
@@ -75,7 +78,6 @@ import { useUploadFile } from "@/hooks/useUploadFile";
|
||||
import { BADGE_AWARD_KIND, BADGE_DEFINITION_KIND, getBadgeATag } from "@/lib/badgeUtils";
|
||||
import { deduplicateEvents } from "@/lib/deduplicateEvents";
|
||||
import { genUserName } from "@/lib/genUserName";
|
||||
import { timeAgo } from "@/lib/timeAgo";
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -446,7 +448,7 @@ function SectionHeader({
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Pending Badge List ────────────────────────────────────────────────────────
|
||||
// ─── Pending Badge Carousel ────────────────────────────────────────────────────
|
||||
|
||||
function PendingBadgeList({
|
||||
pendingBadges,
|
||||
@@ -456,45 +458,35 @@ function PendingBadgeList({
|
||||
badgeMap: Map<string, BadgeDefinition>;
|
||||
}) {
|
||||
const [dismissedATags, setDismissedATags] = useState<Set<string>>(new Set());
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const { toast } = useToast();
|
||||
const { mutate: acceptBadge, isPending: isAccepting } = useAcceptBadge();
|
||||
|
||||
const visibleBadges = pendingBadges.filter(
|
||||
(p) => !dismissedATags.has(p.aTag),
|
||||
);
|
||||
|
||||
// Clamp index when badges are dismissed
|
||||
useEffect(() => {
|
||||
if (currentIndex >= visibleBadges.length && visibleBadges.length > 0) {
|
||||
setCurrentIndex(visibleBadges.length - 1);
|
||||
}
|
||||
}, [currentIndex, visibleBadges.length]);
|
||||
|
||||
const handleDismiss = useCallback((aTag: string) => {
|
||||
setDismissedATags((prev) => new Set(prev).add(aTag));
|
||||
}, []);
|
||||
|
||||
if (visibleBadges.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-2">
|
||||
{visibleBadges.map((pending) => (
|
||||
<PendingBadgeRow
|
||||
key={pending.aTag}
|
||||
pending={pending}
|
||||
badge={badgeMap.get(pending.aTag)}
|
||||
onDismiss={() => handleDismiss(pending.aTag)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PendingBadgeRow({
|
||||
pending,
|
||||
badge,
|
||||
onDismiss,
|
||||
}: {
|
||||
pending: PendingBadge;
|
||||
badge: BadgeDefinition | undefined;
|
||||
onDismiss: () => void;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const { mutate: acceptBadge, isPending: isAccepting } = useAcceptBadge();
|
||||
const current = visibleBadges[currentIndex];
|
||||
const badge = badgeMap.get(current.aTag);
|
||||
const hasPrev = currentIndex > 0;
|
||||
const hasNext = currentIndex < visibleBadges.length - 1;
|
||||
|
||||
const handleAccept = () => {
|
||||
acceptBadge(
|
||||
{ aTag: pending.aTag, awardEventId: pending.awardEvent.id },
|
||||
{ aTag: current.aTag, awardEventId: current.awardEvent.id },
|
||||
{
|
||||
onSuccess: () => toast({ title: "Badge accepted!" }),
|
||||
onError: () =>
|
||||
@@ -503,67 +495,107 @@ function PendingBadgeRow({
|
||||
);
|
||||
};
|
||||
|
||||
const badgeNaddr = nip19.naddrEncode({
|
||||
kind: BADGE_DEFINITION_KIND,
|
||||
pubkey: current.issuerPubkey,
|
||||
identifier: current.identifier,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl border border-border bg-card animate-pending-glow transition-colors">
|
||||
{badge ? (
|
||||
<BadgeThumbnail badge={badge} size={40} className="shrink-0" />
|
||||
) : (
|
||||
<Skeleton className="size-10 rounded-lg shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{badge?.name ?? pending.identifier}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<IssuerName pubkey={pending.issuerPubkey} />
|
||||
<span className="text-muted-foreground text-xs">·</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{timeAgo(pending.awardedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 rounded-2xl border border-border bg-card overflow-hidden animate-pending-glow">
|
||||
{/* Badge showcase -- notification-style presentation */}
|
||||
<Link to={`/${badgeNaddr}`} className="block">
|
||||
{badge?.event ? (
|
||||
<BadgeContent event={badge.event} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center py-10">
|
||||
<Skeleton className="size-28 rounded-2xl" />
|
||||
<div className="mt-4 space-y-2 flex flex-col items-center">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* Issuer line */}
|
||||
<div className="flex items-center justify-center gap-2 pb-2 -mt-1">
|
||||
<span className="text-xs text-muted-foreground">from</span>
|
||||
<IssuerName pubkey={current.issuerPubkey} />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-center gap-3 pb-4 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
className="h-8 gap-1.5"
|
||||
className="rounded-full px-6 h-10 text-sm font-semibold gap-2 shadow-md hover:scale-105 active:scale-95 transition-all"
|
||||
onClick={handleAccept}
|
||||
disabled={isAccepting}
|
||||
style={{ filter: 'drop-shadow(0 2px 8px hsl(var(--primary) / 0.25))' }}
|
||||
>
|
||||
{isAccepting ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="size-3.5" />
|
||||
<>
|
||||
<Award className="size-4" />
|
||||
Accept Badge
|
||||
</>
|
||||
)}
|
||||
Accept
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-8 text-muted-foreground"
|
||||
onClick={onDismiss}
|
||||
className="rounded-full h-10 px-4 text-sm text-muted-foreground"
|
||||
onClick={() => handleDismiss(current.aTag)}
|
||||
disabled={isAccepting}
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Navigation bar */}
|
||||
{visibleBadges.length > 1 && (
|
||||
<div className="flex items-center justify-between border-t border-border px-3 py-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => setCurrentIndex((i) => i - 1)}
|
||||
disabled={!hasPrev}
|
||||
aria-label="Previous badge"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{currentIndex + 1} of {visibleBadges.length}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => setCurrentIndex((i) => i + 1)}
|
||||
disabled={!hasNext}
|
||||
aria-label="Next badge"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PendingBadgeSkeleton() {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl border border-border">
|
||||
<Skeleton className="size-10 rounded-lg shrink-0" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<div className="rounded-2xl border border-border bg-card overflow-hidden">
|
||||
<div className="flex flex-col items-center py-10">
|
||||
<Skeleton className="size-28 rounded-2xl" />
|
||||
<div className="mt-4 space-y-2 flex flex-col items-center">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Skeleton className="h-8 w-20 rounded-md" />
|
||||
<Skeleton className="h-8 w-16 rounded-md" />
|
||||
<div className="flex justify-center gap-3 pb-4 pt-1">
|
||||
<Skeleton className="h-10 w-36 rounded-full" />
|
||||
<Skeleton className="h-10 w-20 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -592,14 +624,11 @@ function AcceptedBadgeList({
|
||||
const { mutate: reorderBadges, isPending: isReordering } = useReorderBadges();
|
||||
const { mutate: removeBadge } = useRemoveBadge();
|
||||
|
||||
const moveUp = useCallback(
|
||||
(index: number) => {
|
||||
if (index <= 0) return;
|
||||
const next = [...refs];
|
||||
[next[index - 1], next[index]] = [next[index], next[index - 1]];
|
||||
setRefs(next);
|
||||
const handleReorder = useCallback(
|
||||
(newRefs: AcceptedRef[]) => {
|
||||
setRefs(newRefs);
|
||||
reorderBadges(
|
||||
next.map((r) => ({ aTag: r.aTag, eTag: r.eTag })),
|
||||
newRefs.map((r) => ({ aTag: r.aTag, eTag: r.eTag })),
|
||||
{
|
||||
onError: () =>
|
||||
toast({
|
||||
@@ -609,27 +638,7 @@ function AcceptedBadgeList({
|
||||
},
|
||||
);
|
||||
},
|
||||
[refs, setRefs, reorderBadges, toast],
|
||||
);
|
||||
|
||||
const moveDown = useCallback(
|
||||
(index: number) => {
|
||||
if (index >= refs.length - 1) return;
|
||||
const next = [...refs];
|
||||
[next[index], next[index + 1]] = [next[index + 1], next[index]];
|
||||
setRefs(next);
|
||||
reorderBadges(
|
||||
next.map((r) => ({ aTag: r.aTag, eTag: r.eTag })),
|
||||
{
|
||||
onError: () =>
|
||||
toast({
|
||||
title: "Failed to reorder badges",
|
||||
variant: "destructive",
|
||||
}),
|
||||
},
|
||||
);
|
||||
},
|
||||
[refs, setRefs, reorderBadges, toast],
|
||||
[setRefs, reorderBadges, toast],
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
@@ -644,92 +653,54 @@ function AcceptedBadgeList({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5 relative mt-2">
|
||||
<div className="relative mt-2">
|
||||
{isReordering && (
|
||||
<div className="absolute inset-0 bg-background/40 backdrop-blur-[1px] z-10 flex items-center justify-center rounded-xl pointer-events-none">
|
||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{refs.map((ref, index) => (
|
||||
<AcceptedBadgeRow
|
||||
key={ref.aTag}
|
||||
ref_={ref}
|
||||
index={index}
|
||||
total={refs.length}
|
||||
badge={badgeMap.get(ref.aTag)}
|
||||
onMoveUp={() => moveUp(index)}
|
||||
onMoveDown={() => moveDown(index)}
|
||||
onRemove={() => handleRemove(ref.aTag)}
|
||||
<ScrollArea className="max-h-[420px]">
|
||||
<SortableList
|
||||
items={refs}
|
||||
getItemId={(ref) => ref.aTag}
|
||||
onReorder={handleReorder}
|
||||
className="space-y-1.5"
|
||||
renderItem={(ref, index) => (
|
||||
<SortableItem
|
||||
key={ref.aTag}
|
||||
id={ref.aTag}
|
||||
className="rounded-xl border border-border bg-card hover:bg-accent/30"
|
||||
draggingClassName="z-10 opacity-80 shadow-lg ring-2 ring-primary/20"
|
||||
>
|
||||
<div className="flex items-center gap-3 p-3">
|
||||
<span className="text-xs font-mono text-muted-foreground w-5 text-center shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
{badgeMap.get(ref.aTag) ? (
|
||||
<BadgeThumbnail badge={badgeMap.get(ref.aTag)!} size={40} className="shrink-0" />
|
||||
) : (
|
||||
<Skeleton className="size-10 rounded-lg shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">
|
||||
{badgeMap.get(ref.aTag)?.name ?? ref.identifier}
|
||||
</span>
|
||||
<IssuerName pubkey={ref.pubkey} />
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-destructive shrink-0"
|
||||
onClick={() => handleRemove(ref.aTag)}
|
||||
aria-label="Remove badge"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</SortableItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AcceptedBadgeRow({
|
||||
ref_,
|
||||
index,
|
||||
total,
|
||||
badge,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onRemove,
|
||||
}: {
|
||||
ref_: AcceptedRef;
|
||||
index: number;
|
||||
total: number;
|
||||
badge: BadgeDefinition | undefined;
|
||||
onMoveUp: () => void;
|
||||
onMoveDown: () => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-3 rounded-xl border border-border bg-card hover:bg-accent/30 transition-colors group">
|
||||
<span className="text-xs font-mono text-muted-foreground w-5 text-center shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
{badge ? (
|
||||
<BadgeThumbnail badge={badge} size={40} className="shrink-0" />
|
||||
) : (
|
||||
<Skeleton className="size-10 rounded-lg shrink-0" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">
|
||||
{badge?.name ?? ref_.identifier}
|
||||
</span>
|
||||
<IssuerName pubkey={ref_.pubkey} />
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 sidebar:opacity-0 sidebar:group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={onMoveUp}
|
||||
disabled={index === 0}
|
||||
aria-label="Move up"
|
||||
>
|
||||
<ChevronUp className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7"
|
||||
onClick={onMoveDown}
|
||||
disabled={index === total - 1}
|
||||
aria-label="Move down"
|
||||
>
|
||||
<ChevronDown className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:text-destructive sidebar:opacity-0 sidebar:group-hover:opacity-100 transition-all shrink-0"
|
||||
onClick={onRemove}
|
||||
aria-label="Remove badge"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -778,15 +749,17 @@ function CreatedBadgeList({ badges }: { badges: ParsedBadge[] }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-2">
|
||||
{badges.map((badge) => (
|
||||
<CreatedBadgeRow
|
||||
key={badge.aTag}
|
||||
badge={badge}
|
||||
onEdit={setEditingBadge}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ScrollArea className="max-h-[420px] mt-2">
|
||||
<div className="space-y-2">
|
||||
{badges.map((badge) => (
|
||||
<CreatedBadgeRow
|
||||
key={badge.aTag}
|
||||
badge={badge}
|
||||
onEdit={setEditingBadge}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user