174 lines
5.6 KiB
TypeScript
174 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* PostActionsFooter — the icon-button row at the bottom of a post card.
|
|
*
|
|
* Left cluster (non-owner): Like · Comment · Reshare
|
|
* Left cluster (owner): Like · Comment · Send
|
|
* Right cluster: Save to Collection (folder-plus)
|
|
*
|
|
* Reshare and Send are *mutually exclusive* — once the post is on your
|
|
* account there's nothing left to reshare onto it, so the Reshare slot
|
|
* is replaced by Send (external share via SharePostSheet). Ownership is
|
|
* decided by `user.id === post.user_id`, which the BFF already includes
|
|
* on every post fetch — no extra round-trip needed.
|
|
*
|
|
* Like / Comment / Reshare are placeholders today (toast on click).
|
|
* Save to Collection mounts the existing CollectionsDrawer in
|
|
* `existingPostId` mode so the post attaches without a duplicate.
|
|
*
|
|
* Auth gating is handled per-action via `useAuthGate` — anonymous users
|
|
* see the in-place auth modal before the underlying action runs.
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
HiOutlineArrowPath,
|
|
HiOutlineChatBubbleOvalLeft,
|
|
HiOutlineFolderPlus,
|
|
HiOutlineHeart,
|
|
HiOutlinePaperAirplane,
|
|
} from "react-icons/hi2";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
import { posts as postsApi, SocialError, type Post } from "@/lib/social";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
import { useAuthGate } from "@/components/auth/auth-gate";
|
|
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
|
import { CollectionsDrawer } from "@/components/selection-toolbar/collections-drawer";
|
|
import { SharePostSheet } from "./share-post-sheet";
|
|
|
|
// All icons come from the Heroicons v2 outline family (react-icons
|
|
// `hi2` namespace), so they share one design language — uniform stroke
|
|
// weight, matching corner radius, matching optical sizing.
|
|
const ICON_CLASS = "size-[1.375rem]";
|
|
|
|
export function PostActionsFooter({ post }: { post: Post }) {
|
|
const { user } = useAuth();
|
|
const { requireAuth } = useAuthGate();
|
|
const isMobile = useMediaQuery("(max-width: 767px)");
|
|
const [collectionsOpen, setCollectionsOpen] = useState(false);
|
|
const [shareOpen, setShareOpen] = useState(false);
|
|
// Optimistic local mirror of post.viewer_has_reshared so the swap
|
|
// happens the instant the user taps Reshare. Reconciles to the
|
|
// server's value via rollback if the API call fails.
|
|
const [reshared, setReshared] = useState<boolean>(
|
|
!!post.viewer_has_reshared,
|
|
);
|
|
|
|
const isOwner = !!user && user.id === post.user_id;
|
|
// Owners and resharers both see Send — the post is "on their account"
|
|
// either way, so the action they need is external share, not reshare.
|
|
const showSend = isOwner || reshared;
|
|
|
|
async function withAuth(fn: () => void | Promise<void>) {
|
|
if (!(await requireAuth())) return;
|
|
await fn();
|
|
}
|
|
|
|
async function onReshare() {
|
|
// Optimistic flip first so the UI feels instant; revert on error.
|
|
setReshared(true);
|
|
try {
|
|
await postsApi.reshare(post.id);
|
|
toast.success("Reshared to your account", { duration: 2000 });
|
|
} catch (err) {
|
|
setReshared(false);
|
|
const msg =
|
|
err instanceof SocialError ? err.message : "Could not reshare";
|
|
toast.error(msg, { duration: 3000 });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center justify-between px-2">
|
|
<div className="flex items-center gap-1">
|
|
<ActionButton
|
|
label="Like"
|
|
onClick={() =>
|
|
withAuth(() => {
|
|
toast("Likes are coming soon", { duration: 2000 });
|
|
})
|
|
}
|
|
icon={<HiOutlineHeart className={ICON_CLASS} />}
|
|
/>
|
|
<ActionButton
|
|
label="Comment"
|
|
onClick={() =>
|
|
withAuth(() => {
|
|
toast("Comments are coming soon", { duration: 2000 });
|
|
})
|
|
}
|
|
icon={<HiOutlineChatBubbleOvalLeft className={ICON_CLASS} />}
|
|
/>
|
|
{/* Reshare for posts not yet on your account, Send for posts
|
|
that are — authored OR previously reshared. Mutually
|
|
exclusive: the row stays three icons regardless of viewer
|
|
state. */}
|
|
{showSend ? (
|
|
<ActionButton
|
|
label="Send"
|
|
onClick={() => setShareOpen(true)}
|
|
icon={<HiOutlinePaperAirplane className={ICON_CLASS} />}
|
|
/>
|
|
) : (
|
|
<ActionButton
|
|
label="Reshare"
|
|
onClick={() => withAuth(onReshare)}
|
|
icon={<HiOutlineArrowPath className={ICON_CLASS} />}
|
|
/>
|
|
)}
|
|
</div>
|
|
<ActionButton
|
|
label="Save to collection"
|
|
onClick={() => withAuth(() => setCollectionsOpen(true))}
|
|
icon={<HiOutlineFolderPlus className={ICON_CLASS} />}
|
|
/>
|
|
</div>
|
|
|
|
<CollectionsDrawer
|
|
open={collectionsOpen}
|
|
onClose={() => setCollectionsOpen(false)}
|
|
isMobile={isMobile}
|
|
existingPostId={post.id}
|
|
/>
|
|
|
|
{showSend ? (
|
|
<SharePostSheet
|
|
open={shareOpen}
|
|
onClose={() => setShareOpen(false)}
|
|
post={post}
|
|
isMobile={isMobile}
|
|
/>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ActionButton({
|
|
label,
|
|
onClick,
|
|
icon,
|
|
}: {
|
|
label: string;
|
|
onClick: () => void;
|
|
icon: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
aria-label={label}
|
|
title={label}
|
|
className={cn(
|
|
"size-10 inline-flex items-center justify-center rounded-full",
|
|
"text-muted-foreground hover:text-foreground hover:bg-muted",
|
|
"transition-colors",
|
|
)}
|
|
>
|
|
{icon}
|
|
</button>
|
|
);
|
|
}
|