mirror of https://github.com/usememos/memos.git
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>
This commit is contained in:
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…
Reference in New Issue