139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* /u/me — owner Posts grid (the default tab).
|
|
*
|
|
* Grid-only — no Grid/Scroll toggle. Tapping a thumbnail jumps to
|
|
* /u/me/posts#post-<id> where the Instagram-style scroll feed renders
|
|
* the full passages.
|
|
*
|
|
* `?edit=1` (set by the header's Edit profile button) swaps the grid
|
|
* for the inline edit form. Saving returns to /u/me. Keeps the URL
|
|
* shareable while avoiding a separate route + layout.
|
|
*/
|
|
|
|
import { Suspense, useEffect, useState } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { LayoutGrid } from "lucide-react";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
import { ProfileEditForm } from "@/components/profile/profile-edit-form";
|
|
import { PostGridCard } from "@/components/social/post-grid-card";
|
|
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
|
import { posts, SocialError, type Post } from "@/lib/social";
|
|
|
|
export default function MyProfilePage() {
|
|
// useSearchParams() forces a CSR bailout during prerender; the
|
|
// Suspense boundary is required by Next.js so the build doesn't fail
|
|
// (the same wrapping is in app/u/me/layout.tsx for its own check).
|
|
// The fallback matches the page's idle loading state so the brief
|
|
// moment before search params resolve doesn't flash an empty section.
|
|
return (
|
|
<Suspense
|
|
fallback={<p className="text-sm text-muted-foreground">Loading…</p>}
|
|
>
|
|
<MyProfilePageInner />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
function MyProfilePageInner() {
|
|
const { user } = useAuth();
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const editMode = searchParams.get("edit") === "1";
|
|
|
|
const [feed, setFeed] = useState<Post[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
// Post id pending confirmation — modal opens when non-null and runs
|
|
// the actual delete on confirm.
|
|
const [pendingDelete, setPendingDelete] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!user) return;
|
|
posts
|
|
.mine()
|
|
.then((rows) => setFeed(rows ?? []))
|
|
.catch((err) =>
|
|
setError(
|
|
err instanceof SocialError ? err.message : "Could not load posts.",
|
|
),
|
|
)
|
|
.finally(() => setLoading(false));
|
|
}, [user]);
|
|
|
|
// Card delete buttons stage an id; ConfirmModal runs `performDelete`
|
|
// when the user confirms. We throw on failure so the modal stays open
|
|
// with an inline error message.
|
|
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);
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
function onPostUpdated(updated: Post) {
|
|
setFeed((rows) => rows.map((p) => (p.id === updated.id ? updated : p)));
|
|
}
|
|
|
|
if (!user) return null;
|
|
|
|
if (editMode) {
|
|
return <ProfileEditForm user={user} onDone={() => router.replace("/u/me")} />;
|
|
}
|
|
|
|
return (
|
|
<section>
|
|
{error ? (
|
|
<p className="text-sm text-destructive">{error}</p>
|
|
) : loading ? (
|
|
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
) : feed.length === 0 ? (
|
|
<EmptyPosts />
|
|
) : (
|
|
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
{feed.map((p) => (
|
|
<PostGridCard
|
|
key={p.id}
|
|
post={p}
|
|
onDelete={(id) => setPendingDelete(id)}
|
|
onUpdated={onPostUpdated}
|
|
href={`/u/me/posts#post-${p.id}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<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}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function EmptyPosts() {
|
|
return (
|
|
<div className="flex flex-col items-center text-center gap-3 py-12 text-muted-foreground">
|
|
<LayoutGrid className="size-8" />
|
|
<p className="text-sm">
|
|
You haven't posted anything yet. Select any passage in the
|
|
knowledgebase and use the toolbar to share it to your feed.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|