claude fixed up the ui
This commit is contained in:
@@ -466,6 +466,30 @@ html, body {
|
|||||||
.mx-action-delete:hover { color: var(--mx-red); background: color-mix(in oklch, var(--mx-red) 10%, transparent); }
|
.mx-action-delete:hover { color: var(--mx-red); background: color-mix(in oklch, var(--mx-red) 10%, transparent); }
|
||||||
.mx-action-confirm { color: var(--mx-red) !important; background: color-mix(in oklch, var(--mx-red) 15%, transparent) !important; }
|
.mx-action-confirm { color: var(--mx-red) !important; background: color-mix(in oklch, var(--mx-red) 15%, transparent) !important; }
|
||||||
|
|
||||||
|
.mx-follow-btn {
|
||||||
|
padding: 0.25rem 0.875rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1.5px solid var(--mx-border2);
|
||||||
|
background: none;
|
||||||
|
color: var(--mx-fg);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.mx-follow-btn:hover:not(:disabled) { background: var(--mx-surface2); }
|
||||||
|
.mx-follow-btn--following {
|
||||||
|
background: var(--mx-surface);
|
||||||
|
color: var(--mx-muted);
|
||||||
|
}
|
||||||
|
.mx-follow-btn--following:hover:not(:disabled) {
|
||||||
|
background: color-mix(in oklch, var(--mx-red) 10%, transparent);
|
||||||
|
border-color: var(--mx-red);
|
||||||
|
color: var(--mx-red);
|
||||||
|
}
|
||||||
|
.mx-follow-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
.mx-tweet-text {
|
.mx-tweet-text {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|||||||
@@ -896,8 +896,7 @@ function RefreshButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FollowButton({ targetUserId, amIFollowing }: { targetUserId: string; amIFollowing: boolean }) {
|
function useFollowUser(targetUserId: string) {
|
||||||
const { userId: currentUserId } = useContext(AuthCtx);
|
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
const followMutation = useMutation({
|
const followMutation = useMutation({
|
||||||
@@ -928,35 +927,42 @@ function FollowButton({ targetUserId, amIFollowing }: { targetUserId: string; am
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!currentUserId || currentUserId === targetUserId) return null;
|
return {
|
||||||
|
follow: () => followMutation.mutate(),
|
||||||
const isPending = followMutation.isPending || unfollowMutation.isPending;
|
unfollow: () => unfollowMutation.mutate(),
|
||||||
|
isPending: followMutation.isPending || unfollowMutation.isPending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function FollowButton({ amIFollowing, isPending, onToggle }: { amIFollowing: boolean; isPending: boolean; onToggle: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`mx-action-btn${amIFollowing ? " mx-action-btn--active" : ""}`}
|
className={`mx-follow-btn${amIFollowing ? " mx-follow-btn--following" : ""}`}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={(e) => {
|
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
||||||
e.stopPropagation();
|
|
||||||
amIFollowing ? unfollowMutation.mutate() : followMutation.mutate();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{isPending ? "..." : amIFollowing ? "Unfollow" : "Follow"}
|
{isPending ? "…" : amIFollowing ? "Unfollow" : "Follow"}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserCard({ user }: { user: User }) {
|
function UserCard({ user }: { user: User }) {
|
||||||
|
const { userId: currentUserId } = useContext(AuthCtx);
|
||||||
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null);
|
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const { follow, unfollow, isPending } = useFollowUser(user.id);
|
||||||
|
|
||||||
const userUrl = `${window.location.origin}/users/${user.id}`;
|
const userUrl = `${window.location.origin}/users/${user.id}`;
|
||||||
|
const canFollow = !!currentUserId && currentUserId !== user.id;
|
||||||
|
const amIFollowing = user.amIFollowing ?? false;
|
||||||
|
|
||||||
const ctxItems: ContextMenuItem[] = [
|
const ctxItems: ContextMenuItem[] = [
|
||||||
{
|
{ type: "item", label: "Share", onClick: () => navigator.clipboard.writeText(userUrl) },
|
||||||
type: "item",
|
...(canFollow ? [
|
||||||
label: "Share",
|
{ type: "separator" as const },
|
||||||
onClick: () => navigator.clipboard.writeText(userUrl),
|
amIFollowing
|
||||||
},
|
? { type: "item" as const, label: "Unfollow", onClick: unfollow }
|
||||||
|
: { type: "item" as const, label: "Follow", onClick: follow },
|
||||||
|
] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -972,7 +978,6 @@ function UserCard({ user }: { user: User }) {
|
|||||||
<div className="mx-tweet-body">
|
<div className="mx-tweet-body">
|
||||||
<div className="mx-tweet-header">
|
<div className="mx-tweet-header">
|
||||||
<span className="mx-tweet-handle">{user.email}</span>
|
<span className="mx-tweet-handle">{user.email}</span>
|
||||||
<FollowButton targetUserId={user.id} amIFollowing={user.amIFollowing ?? false} />
|
|
||||||
</div>
|
</div>
|
||||||
{(user.followerCount !== undefined || user.followingCount !== undefined) && (
|
{(user.followerCount !== undefined || user.followingCount !== undefined) && (
|
||||||
<div className="mx-tweet-meta" style={{ fontSize: "0.8rem", color: "var(--mx-muted)", marginTop: "4px" }}>
|
<div className="mx-tweet-meta" style={{ fontSize: "0.8rem", color: "var(--mx-muted)", marginTop: "4px" }}>
|
||||||
@@ -981,13 +986,13 @@ function UserCard({ user }: { user: User }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{canFollow && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", flexShrink: 0 }}>
|
||||||
|
<FollowButton amIFollowing={amIFollowing} isPending={isPending} onToggle={amIFollowing ? unfollow : follow} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<ContextMenu
|
<ContextMenu x={ctxMenu.x} y={ctxMenu.y} items={ctxItems} onClose={() => setCtxMenu(null)} />
|
||||||
x={ctxMenu.x}
|
|
||||||
y={ctxMenu.y}
|
|
||||||
items={ctxItems}
|
|
||||||
onClose={() => setCtxMenu(null)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
@@ -1032,6 +1037,8 @@ function UserList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserDetail({ userId }: { userId: string }) {
|
function UserDetail({ userId }: { userId: string }) {
|
||||||
|
const { userId: currentUserId } = useContext(AuthCtx);
|
||||||
|
const { follow, unfollow, isPending } = useFollowUser(userId);
|
||||||
const { data: user, isLoading, isError } = useQuery({
|
const { data: user, isLoading, isError } = useQuery({
|
||||||
queryKey: ["user", userId],
|
queryKey: ["user", userId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -1049,6 +1056,9 @@ function UserDetail({ userId }: { userId: string }) {
|
|||||||
if (isLoading) return <Spinner />;
|
if (isLoading) return <Spinner />;
|
||||||
if (isError || !user) return <ErrorBanner message="Could not load user" />;
|
if (isError || !user) return <ErrorBanner message="Could not load user" />;
|
||||||
|
|
||||||
|
const canFollow = !!currentUserId && currentUserId !== userId;
|
||||||
|
const amIFollowing = user.amIFollowing ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-detail">
|
<div className="mx-detail">
|
||||||
<div className="mx-detail-header">
|
<div className="mx-detail-header">
|
||||||
@@ -1064,10 +1074,12 @@ function UserDetail({ userId }: { userId: string }) {
|
|||||||
<div className="mx-tweet-avatar">
|
<div className="mx-tweet-avatar">
|
||||||
<span>M</span>
|
<span>M</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||||
<span className="mx-tweet-handle">{user.email}</span>
|
<span className="mx-tweet-handle">{user.email}</span>
|
||||||
<FollowButton targetUserId={user.id} amIFollowing={user.amIFollowing ?? false} />
|
{canFollow && (
|
||||||
|
<FollowButton amIFollowing={amIFollowing} isPending={isPending} onToggle={amIFollowing ? unfollow : follow} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: "0.85rem", color: "var(--mx-muted)", marginTop: "6px", display: "flex", gap: "16px" }}>
|
<div style={{ fontSize: "0.85rem", color: "var(--mx-muted)", marginTop: "6px", display: "flex", gap: "16px" }}>
|
||||||
<span><strong>{user.followerCount ?? 0}</strong> followers</span>
|
<span><strong>{user.followerCount ?? 0}</strong> followers</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user