feat(web): enhance inbox notifications and user profile layouts

- Polish inbox notification items with improved visual hierarchy
  - Add original memo snippet with left border indicator
  - Redesign comment preview with gradient background and primary accent
  - Increase spacing and improve typography with consistent sizing
  - Add ring borders to avatars and refined icon badges
  - Enhance loading and error states with better skeleton designs
  - Improve hover states and transitions throughout

- Redesign user profile header layout
  - Create full-width centered header with avatar and user info
  - Add horizontal layout for profile actions
  - Improve responsive design with proper flex wrapping
  - Allow memo list to use full width for masonry layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
pull/5226/head
Steven 2 weeks ago
parent 71d0dbaf41
commit 8f0658e90d

@ -91,12 +91,13 @@ const MemoCommentMessage = observer(({ notification }: Props) => {
if (!initialized && !hasError) {
return (
<div className="w-full px-4 py-3.5 border-b border-border last:border-b-0 bg-muted/20 animate-pulse">
<div className="w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-muted/10 animate-pulse">
<div className="flex items-start gap-3">
<div className="w-9 h-9 rounded-full bg-muted/60 shrink-0" />
<div className="flex-1 space-y-2.5">
<div className="h-3.5 bg-muted/60 rounded w-2/5" />
<div className="h-16 bg-muted/40 rounded-md" />
<div className="w-10 h-10 rounded-full bg-muted/50 shrink-0" />
<div className="flex-1 space-y-3">
<div className="h-4 bg-muted/50 rounded-md w-2/5" />
<div className="h-3 bg-muted/40 rounded-md w-3/4" />
<div className="h-20 bg-muted/30 rounded-xl" />
</div>
</div>
</div>
@ -105,20 +106,20 @@ const MemoCommentMessage = observer(({ notification }: Props) => {
if (hasError) {
return (
<div className="w-full px-4 py-3.5 border-b border-border last:border-b-0 bg-destructive/[0.03]">
<div className="w-full px-5 py-4 border-b border-border/60 last:border-b-0 bg-destructive/[0.04] group">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-destructive/10 flex items-center justify-center shrink-0">
<XIcon className="w-4 h-4 text-destructive" />
<div className="w-10 h-10 rounded-full bg-destructive/15 flex items-center justify-center shrink-0 ring-1 ring-destructive/20">
<XIcon className="w-5 h-5 text-destructive" strokeWidth={2} />
</div>
<span className="text-sm text-destructive/90">{t("inbox.failed-to-load")}</span>
<span className="text-sm text-destructive/80 font-medium">{t("inbox.failed-to-load")}</span>
</div>
<button
onClick={handleDeleteMessage}
className="p-1.5 hover:bg-destructive/10 rounded-md transition-colors"
className="p-1.5 hover:bg-destructive/15 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.delete")}
>
<TrashIcon className="w-3.5 h-3.5 text-destructive/70 hover:text-destructive" />
<TrashIcon className="w-4 h-4 text-destructive/70 hover:text-destructive transition-colors" strokeWidth={2} />
</button>
</div>
</div>
@ -130,71 +131,86 @@ const MemoCommentMessage = observer(({ notification }: Props) => {
return (
<div
className={cn(
"w-full px-4 py-3.5 border-b border-border last:border-b-0 transition-colors group relative",
isUnread ? "bg-primary/[0.02] hover:bg-primary/[0.04]" : "hover:bg-muted/40",
"w-full px-5 py-4 border-b border-border/60 last:border-b-0 transition-all duration-200 group relative",
isUnread ? "bg-primary/[0.03] hover:bg-primary/[0.05]" : "hover:bg-muted/30",
)}
>
{/* Unread indicator bar */}
{isUnread && <div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />}
{isUnread && <div className="absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary to-primary/60" />}
<div className="flex items-start gap-3">
{/* Avatar & Icon */}
<div className="relative shrink-0 mt-0.5">
<UserAvatar className="w-9 h-9" avatarUrl={sender?.avatarUrl} />
<div className="relative shrink-0">
<UserAvatar className="w-10 h-10 ring-1 ring-border/40" avatarUrl={sender?.avatarUrl} />
<div
className={cn(
"absolute -bottom-0.5 -right-0.5 w-[18px] h-[18px] rounded-full border-[2px] border-background flex items-center justify-center shadow-sm",
isUnread ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground",
"absolute -bottom-1 -right-1 w-5 h-5 rounded-full border-2 border-background flex items-center justify-center shadow-md transition-all",
isUnread ? "bg-primary text-primary-foreground" : "bg-muted/80 text-muted-foreground",
)}
>
<MessageCircleIcon className="w-2.5 h-2.5" />
<MessageCircleIcon className="w-2.5 h-2.5" strokeWidth={2.5} />
</div>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0 flex items-baseline gap-1.5 flex-wrap">
<span className="font-semibold text-sm text-foreground">{sender?.displayName || sender?.username}</span>
<span className="text-sm text-muted-foreground">commented on your memo</span>
<span className="text-xs text-muted-foreground/80">
· {notification.createTime?.toLocaleDateString([], { month: "short", day: "numeric" })} at{" "}
<div className="flex items-center justify-between gap-3 mb-1">
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
<span className="font-semibold text-sm text-foreground/95">{sender?.displayName || sender?.username}</span>
<span className="text-sm text-muted-foreground/80">commented on your memo</span>
<span className="text-xs text-muted-foreground/60">
{notification.createTime?.toLocaleDateString([], { month: "short", day: "numeric" })} at{" "}
{notification.createTime?.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</span>
</div>
<div className="flex items-center gap-0.5 shrink-0">
<div className="flex items-center gap-1 shrink-0">
{isUnread ? (
<button
onClick={() => handleArchiveMessage()}
className="p-1.5 hover:bg-background/80 rounded-md transition-all opacity-0 group-hover:opacity-100"
className="p-1.5 hover:bg-primary/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.archive")}
>
<CheckIcon className="w-3.5 h-3.5 text-muted-foreground hover:text-primary" />
<CheckIcon className="w-4 h-4 text-muted-foreground hover:text-primary transition-colors" strokeWidth={2} />
</button>
) : (
<button
onClick={handleDeleteMessage}
className="p-1.5 hover:bg-background/80 rounded-md transition-all opacity-0 group-hover:opacity-100"
className="p-1.5 hover:bg-destructive/10 rounded-lg transition-all duration-150 opacity-0 group-hover:opacity-100"
title={t("common.delete")}
>
<TrashIcon className="w-3.5 h-3.5 text-muted-foreground hover:text-destructive" />
<TrashIcon className="w-4 h-4 text-muted-foreground hover:text-destructive transition-colors" strokeWidth={2} />
</button>
)}
</div>
</div>
{/* Original Memo Snippet */}
{relatedMemo && (
<div className="pl-3 border-l-2 border-muted-foreground/20 mb-3">
<p className="text-sm text-foreground/60 line-clamp-1 leading-relaxed">
<span className="text-xs text-muted-foreground/50 font-medium mr-2 uppercase tracking-wide">Original:</span>
{relatedMemo.content || <span className="italic text-muted-foreground/40">Empty memo</span>}
</p>
</div>
)}
{/* Comment Preview */}
{commentMemo && (
<div
onClick={handleNavigateToMemo}
className="mt-2 p-3 rounded-md bg-muted/40 hover:bg-muted/60 cursor-pointer border border-border/50 hover:border-border transition-all group/comment"
className="p-2 sm:p-3 rounded-lg bg-gradient-to-br from-primary/[0.06] to-primary/[0.03] hover:from-primary/[0.1] hover:to-primary/[0.06] cursor-pointer border border-primary/30 hover:border-primary/50 transition-all duration-200 group/comment shadow-sm hover:shadow"
>
<div className="flex items-start gap-2">
<MessageCircleIcon className="w-3.5 h-3.5 text-muted-foreground/60 shrink-0 mt-0.5" />
<p className="text-[13px] text-foreground/90 line-clamp-2 leading-relaxed group-hover/comment:text-foreground transition-colors">
{commentMemo.content || <span className="italic text-muted-foreground">Empty comment</span>}
</p>
<div className="w-5 h-5 flex items-center justify-center shrink-0">
<MessageCircleIcon className="w-4 h-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-primary/60 font-semibold mb-1 uppercase tracking-wider">Comment</p>
<p className="text-sm text-foreground/90 line-clamp-2">
{commentMemo.content || <span className="italic text-muted-foreground/50">Empty comment</span>}
</p>
</div>
</div>
</div>
)}

@ -64,39 +64,47 @@ const UserProfile = observer(() => {
};
return (
<section className="w-full max-w-3xl mx-auto min-h-full flex flex-col justify-start items-center pb-8">
<div className="w-full flex flex-col justify-start items-center max-w-2xl">
{!loadingState.isLoading &&
(user ? (
<>
<div className="my-4 w-full flex justify-end items-center gap-2">
<Button variant="outline" onClick={handleCopyProfileLink}>
<section className="w-full min-h-full flex flex-col justify-start items-center">
{!loadingState.isLoading &&
(user ? (
<>
{/* User profile header - centered with max width */}
<div className="w-full max-w-4xl mx-auto mb-8">
<div className="w-full flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 py-6 border-b border-border">
<div className="flex items-center gap-4">
<UserAvatar className="w-20! h-20! drop-shadow rounded-full" avatarUrl={user?.avatarUrl} />
<div className="flex flex-col justify-center items-start">
<h1 className="text-2xl sm:text-3xl font-semibold text-foreground">{user.displayName || user.username}</h1>
{user.username && user.displayName && <p className="text-sm text-muted-foreground">@{user.username}</p>}
</div>
</div>
<Button variant="outline" onClick={handleCopyProfileLink} className="shrink-0">
{t("common.share")}
<ExternalLinkIcon className="ml-1 w-4 h-auto opacity-60" />
</Button>
</div>
<div className="w-full flex flex-col justify-start items-start pt-4 pb-8 px-3">
<UserAvatar className="w-16! h-16! drop-shadow rounded-3xl" avatarUrl={user?.avatarUrl} />
<div className="mt-2 w-auto max-w-[calc(100%-6rem)] flex flex-col justify-center items-start">
<p className="w-full text-3xl text-foreground leading-tight font-medium opacity-80 truncate">
{user.displayName || user.username}
</p>
<p className="w-full text-muted-foreground leading-snug whitespace-pre-wrap truncate line-clamp-6">{user.description}</p>
{user.description && (
<div className="py-4">
<p className="text-base text-foreground/80 whitespace-pre-wrap">{user.description}</p>
</div>
</div>
<PagedMemoList
renderer={(memo: Memo, context?: MemoRenderContext) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact={context?.compact} />
)}
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
/>
</>
) : (
<p>Not found</p>
))}
</div>
)}
</div>
{/* Memo list - full width for proper masonry layout */}
<PagedMemoList
renderer={(memo: Memo, context?: MemoRenderContext) => (
<MemoView key={`${memo.name}-${memo.displayTime}`} memo={memo} showVisibility showPinned compact={context?.compact} />
)}
listSort={listSort}
orderBy={orderBy}
filter={memoFilter}
/>
</>
) : (
<div className="w-full max-w-3xl mx-auto">
<p className="text-center text-muted-foreground mt-8">Not found</p>
</div>
))}
</section>
);
});

Loading…
Cancel
Save