qdpqut37t: Add the frosted bottom dock that likes and comments on the post on screen
This commit is contained in:
@@ -40,10 +40,16 @@ export interface FeedViewProps {
|
||||
now?: number;
|
||||
/** Another page can load; the phone feed ends with a loading slide. */
|
||||
hasMore?: boolean;
|
||||
/** Phone layout only: the slide on screen changed (for the dock). */
|
||||
/** Phone layout only: experts for the featured slide after the second post. */
|
||||
featuredExperts?: DiscoverExpert[];
|
||||
/** Phone layout only: the slide on screen changed (for the dock). */
|
||||
onCurrentChange?: (slide: CurrentSlide) => void;
|
||||
/** Phone layout only: the dock's comments button, for the post on screen. */
|
||||
onOpenComments?: (postId: string) => void;
|
||||
/** Phone layout only: the dock's new-post button. */
|
||||
onOpenComposer?: () => void;
|
||||
/** Phone layout only: the dock's More button (the Discover sheet). */
|
||||
onOpenMore?: () => void;
|
||||
}
|
||||
|
||||
function Spinner() {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { commentsLabel } from "@/lib/feed-model";
|
||||
import { SparkleButton } from "./Buttons";
|
||||
import type { CurrentSlide } from "./MobileFeed";
|
||||
import type { CardPost } from "@/lib/feed-model";
|
||||
|
||||
const icon = {
|
||||
viewBox: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
strokeWidth: 1.7,
|
||||
strokeLinecap: "round" as const,
|
||||
strokeLinejoin: "round" as const,
|
||||
"aria-hidden": true,
|
||||
};
|
||||
|
||||
export interface MobileDockProps {
|
||||
/** The slide on screen; like and comments act on its post. */
|
||||
slide: CurrentSlide;
|
||||
onToggleLike?: (post: CardPost) => void;
|
||||
likeBusy?: boolean;
|
||||
onOpenComments?: (postId: string) => void;
|
||||
onOpenComposer?: () => void;
|
||||
onOpenMore?: () => void;
|
||||
}
|
||||
|
||||
/** What the like and comment buttons show and whether they work, for the slide on screen. */
|
||||
export function dockState(slide: CurrentSlide, opts: { canLike?: boolean; likeBusy?: boolean } = {}) {
|
||||
const post = slide.kind === "post" ? slide.post : null;
|
||||
if (!post) {
|
||||
return { post: null, liked: false, likes: null, comments: null, likeLabel: "Like", commentsLabel: "Comments", likeDisabled: true, commentsDisabled: true };
|
||||
}
|
||||
return {
|
||||
post,
|
||||
liked: post.isLiked,
|
||||
likes: post.likesCount,
|
||||
comments: post.commentsCount,
|
||||
likeLabel: post.isLiked ? `Unlike, ${post.likesCount} likes` : `Like, ${post.likesCount} likes`,
|
||||
commentsLabel: `Open comments, ${commentsLabel(post.commentsCount).toLowerCase()}`,
|
||||
likeDisabled: !post.id || !opts.canLike || !!opts.likeBusy,
|
||||
commentsDisabled: !post.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Phone dock (spec "Mobile layout" > "Bottom dock"): a frosted bar 16px above the
|
||||
* bottom. Like and comments act on the post on screen and are disabled on the
|
||||
* featured and loading slides. Liked state and counts come from the post itself,
|
||||
* the same source as the desktop like button and double-tap.
|
||||
*/
|
||||
export function MobileDock({ slide, onToggleLike, likeBusy, onOpenComments, onOpenComposer, onOpenMore }: MobileDockProps) {
|
||||
const s = dockState(slide, { canLike: !!onToggleLike, likeBusy });
|
||||
const [beat, setBeat] = useState(false);
|
||||
return (
|
||||
<nav className="dock" aria-label="Post actions" data-testid="feed-dock">
|
||||
<button
|
||||
type="button"
|
||||
className={`dk dk-like${s.liked ? " dk-like-on" : ""}${beat && s.liked ? " dk-beat" : ""}`}
|
||||
aria-pressed={s.liked}
|
||||
aria-label={s.likeLabel}
|
||||
disabled={s.likeDisabled}
|
||||
onClick={() => {
|
||||
if (!s.post) return;
|
||||
setBeat(!s.post.isLiked);
|
||||
onToggleLike?.(s.post);
|
||||
}}
|
||||
onAnimationEnd={() => setBeat(false)}
|
||||
>
|
||||
<svg {...icon} fill={s.liked ? "currentColor" : "none"}>
|
||||
<path d="M12 20.5s-7.5-4.4-9.4-9.1C1.3 8 3.1 4.5 6.6 4.5c2 0 3.5 1.1 4.3 2.4.5.7 1.7.7 2.2 0 .8-1.3 2.3-2.4 4.3-2.4 3.5 0 5.3 3.5 4 6.9-1.9 4.7-9.4 9.1-9.4 9.1z" />
|
||||
</svg>
|
||||
{s.likes !== null ? <span className="dk-n" data-testid="dock-likes">{s.likes}</span> : null}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="dk dk-comments"
|
||||
aria-label={s.commentsLabel}
|
||||
disabled={s.commentsDisabled}
|
||||
onClick={() => {
|
||||
if (s.post?.id) onOpenComments?.(s.post.id);
|
||||
}}
|
||||
>
|
||||
<svg {...icon}>
|
||||
<path d="M21 12a8.5 8.5 0 0 1-12.4 7.6L3.5 21l1.4-4.6A8.5 8.5 0 1 1 21 12z" />
|
||||
</svg>
|
||||
{s.comments !== null ? <span className="dk-n" data-testid="dock-comments">{s.comments}</span> : null}
|
||||
</button>
|
||||
<SparkleButton variant="fab" label="New post" className="dk-plus" onClick={() => onOpenComposer?.()} />
|
||||
<button type="button" className="dk dk-more" aria-label="View more" onClick={() => onOpenMore?.()}>
|
||||
<svg {...icon}>
|
||||
<rect x="3.5" y="3.5" width="7" height="7" rx="2" />
|
||||
<rect x="13.5" y="3.5" width="7" height="7" rx="2" />
|
||||
<rect x="3.5" y="13.5" width="7" height="7" rx="2" />
|
||||
<rect x="13.5" y="13.5" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
More
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { HexAvatar } from "./HexAvatar";
|
||||
import { PlayIcon, PostMenu } from "./PostCard";
|
||||
import { VideoPlayer } from "./VideoPlayer";
|
||||
import { FeaturedCarousel } from "./FeaturedCarousel";
|
||||
import { MobileDock } from "./MobileDock";
|
||||
import type { DiscoverExpert } from "@/lib/discover";
|
||||
import type { FeedViewProps } from "./FeedView";
|
||||
|
||||
@@ -415,6 +416,16 @@ export function MobileFeed(p: FeedViewProps) {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{p.status !== "error" ? (
|
||||
<MobileDock
|
||||
slide={{ index, kind, post: currentPost }}
|
||||
onToggleLike={p.onToggleLike}
|
||||
likeBusy={currentPost ? p.likeBusy?.has(currentPost.key) : false}
|
||||
onOpenComments={p.onOpenComments}
|
||||
onOpenComposer={p.onOpenComposer}
|
||||
onOpenMore={p.onOpenMore}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user