109 lines
3.5 KiB
TypeScript
109 lines
3.5 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, and `scroll-mt-*` on each
|
|
* card spaces it below the sticky header so the anchor lands cleanly.
|
|
*
|
|
* Owner controls (Delete + Update background) are mounted per-card.
|
|
*/
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
import { PostScrollCard } from "@/components/social/post-scroll-card";
|
|
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
|
import { posts, SocialError, type Post } from "@/lib/social";
|
|
|
|
export default function MyPostsFeedPage() {
|
|
const { user } = useAuth();
|
|
const [feed, setFeed] = useState<Post[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [pendingDelete, setPendingDelete] = useState<string | null>(null);
|
|
const scrolledToHashRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
setLoading(true);
|
|
posts
|
|
.mine()
|
|
.then((rows) => setFeed(rows ?? []))
|
|
.catch((err) =>
|
|
setError(
|
|
err instanceof SocialError ? err.message : "Could not load posts.",
|
|
),
|
|
)
|
|
.finally(() => setLoading(false));
|
|
}, [user]);
|
|
|
|
// Hash anchor: when the feed mounts, jump to the post identified in the
|
|
// URL hash. Defer one tick so the cards have laid out and `scroll-mt-*`
|
|
// can position the anchor below the sticky header.
|
|
useEffect(() => {
|
|
if (loading || feed.length === 0 || scrolledToHashRef.current) return;
|
|
if (typeof window === "undefined") return;
|
|
const hash = window.location.hash;
|
|
if (!hash) return;
|
|
scrolledToHashRef.current = true;
|
|
requestAnimationFrame(() => {
|
|
const el = document.getElementById(hash.slice(1));
|
|
if (el) el.scrollIntoView({ block: "start", behavior: "auto" });
|
|
});
|
|
}, [loading, feed.length]);
|
|
|
|
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)));
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<div className="max-w-xl w-full mx-auto space-y-6">
|
|
{error ? (
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
) : loading ? (
|
|
<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}
|
|
/>
|
|
))
|
|
)}
|
|
|
|
<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>
|
|
);
|
|
}
|