100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* /u/me/posts — the owner's Instagram-style scroll feed.
|
|
*
|
|
* Reached by tapping a thumbnail from the /u/me Posts grid. The clicked
|
|
* post's id arrives as a `#post-<id>` hash; usePostScrollFeed fetches that
|
|
* post directly so it lands at the top instantly (even for old posts), then
|
|
* keyset-pages older posts beneath it as you scroll.
|
|
*
|
|
* Owner controls (Delete + Update background) are mounted per-card.
|
|
*/
|
|
|
|
import { useState } from "react";
|
|
import { Loader2 } from "lucide-react";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
import { PostScrollCard } from "@/components/social/post/post-scroll-card";
|
|
import { usePostScrollFeed } from "@/components/social/post/use-post-scroll-feed";
|
|
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
|
import { posts, SocialError, type Post } from "@/lib/social";
|
|
|
|
export default function MyPostsFeedPage() {
|
|
const { user } = useAuth();
|
|
// Defer the feed (which calls the authed /posts/mine) until we have a user.
|
|
if (!user) return null;
|
|
return <OwnerScrollFeed />;
|
|
}
|
|
|
|
function OwnerScrollFeed() {
|
|
const { feed, setFeed, loading, hasMore, error, setError, sentinelRef } =
|
|
usePostScrollFeed((before, limit) =>
|
|
posts.mine({ limit, before: before ?? undefined }),
|
|
);
|
|
const [pendingDelete, setPendingDelete] = useState<string | null>(null);
|
|
|
|
async function performDelete() {
|
|
const id = pendingDelete;
|
|
if (!id) return;
|
|
const prev = feed;
|
|
setFeed((rows) => rows.filter((p) => p.id !== id));
|
|
try {
|
|
await posts.delete(id);
|
|
} catch (err) {
|
|
setFeed(prev);
|
|
const msg =
|
|
err instanceof SocialError ? err.message : "Could not delete.";
|
|
setError(msg);
|
|
// Re-throw so ConfirmModal surfaces the failure and stays open.
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
function onPostUpdated(updated: Post) {
|
|
setFeed((rows) => rows.map((p) => (p.id === updated.id ? updated : p)));
|
|
}
|
|
|
|
// Hiding a post moves it off the timeline (into the Hidden tab), so drop
|
|
// it from this feed immediately rather than waiting for a refresh.
|
|
function onHiddenChange(id: string, hidden: boolean) {
|
|
if (hidden) setFeed((rows) => rows.filter((p) => p.id !== id));
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-xl w-full mx-auto space-y-6">
|
|
{error ? (
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
) : loading && feed.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
) : feed.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No posts yet.</p>
|
|
) : (
|
|
feed.map((p) => (
|
|
<PostScrollCard
|
|
key={p.id}
|
|
post={p}
|
|
onDelete={(id) => setPendingDelete(id)}
|
|
onUpdated={onPostUpdated}
|
|
onHiddenChange={onHiddenChange}
|
|
/>
|
|
))
|
|
)}
|
|
|
|
{hasMore && feed.length > 0 ? (
|
|
<div ref={sentinelRef} className="flex justify-center py-6">
|
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : null}
|
|
|
|
<ConfirmModal
|
|
open={!!pendingDelete}
|
|
onClose={() => setPendingDelete(null)}
|
|
title="Delete this post?"
|
|
message="The post will be removed permanently from your feed and from any collections it's in. This can't be undone."
|
|
confirmLabel="Delete post"
|
|
onConfirm={performDelete}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|