chore: code refactor
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* PostGridCard — square card used in the 2-per-row grid view.
|
||||
*
|
||||
* • With image: first attached image fills the card; a bottom-up dark
|
||||
* gradient makes the caption legible without dropping the image's
|
||||
* atmosphere.
|
||||
* • Without image: a palette-aware Tailwind gradient stands in; the
|
||||
* caption sits as a serif quote bottom-left in the foreground colour.
|
||||
*
|
||||
* `onDelete` shows a small X in the corner for owner views. Used both for
|
||||
* actual post deletion (on /u/me) and for removing from a collection
|
||||
* (on /u/me/collections/[id]); the parent decides which.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ImageIcon, X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Post } from "@/lib/social";
|
||||
import { displaySourceLabel } from "@/lib/source-reference";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { postSourceHref, quoteCardBgStyle, quoteSize } from "./post-card-shared";
|
||||
import { UpdateBackgroundModal } from "../update-background-modal";
|
||||
|
||||
export function PostGridCard({
|
||||
post,
|
||||
onDelete,
|
||||
onUpdated,
|
||||
href: hrefProp,
|
||||
}: {
|
||||
post: Post;
|
||||
onDelete?: (id: string) => void;
|
||||
/** Owner-only: shown when defined so the user can swap/remove the
|
||||
* card's background image. */
|
||||
onUpdated?: (updated: Post) => void;
|
||||
/** Override the click target. Defaults to the source-passage URL.
|
||||
* Profile grids pass a `/u/<handle>/posts#post-<id>` anchor so the
|
||||
* thumbnail jumps to the Instagram-style scroll feed instead of the
|
||||
* knowledgebase page. */
|
||||
href?: string;
|
||||
}) {
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const [bgModal, setBgModal] = useState(false);
|
||||
const firstImage = post.images?.[0]?.url;
|
||||
const href = hrefProp ?? postSourceHref(post);
|
||||
const size = quoteSize(post.caption, "grid");
|
||||
// Image variant always anchors text bottom-left over the image's dark
|
||||
// overlay. No-image variant lets `size.alignment` decide — short quotes
|
||||
// sit centred so the gradient frames them; long quotes fall back to
|
||||
// bottom-left so they don't cover the whole card.
|
||||
const bottomLeft = !!firstImage || size.alignment === "bottom-left";
|
||||
|
||||
return (
|
||||
<div className="relative group select-none">
|
||||
<Link
|
||||
href={href}
|
||||
className={cn(
|
||||
"block aspect-square rounded-2xl overflow-hidden ring-1 ring-foreground/10",
|
||||
"hover:ring-foreground/25 hover:shadow-md transition-all relative",
|
||||
)}
|
||||
>
|
||||
{firstImage ? (
|
||||
<>
|
||||
<Image
|
||||
src={firstImage}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 640px) 50vw, 320px"
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
{/* Full-coverage scrim — even the top of the card carries enough
|
||||
darkness (~40%) so white text overlaid anywhere stays
|
||||
legible without looking blended with the image. */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/95 via-black/70 to-black/40" />
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0" style={quoteCardBgStyle()} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex flex-col p-4 sm:p-5",
|
||||
// Text color: white on the dark image overlay, theme
|
||||
// foreground on the flat quote-card background.
|
||||
firstImage ? "text-white" : "text-foreground",
|
||||
bottomLeft
|
||||
? "justify-end items-start text-left"
|
||||
: "justify-center items-center text-center",
|
||||
)}
|
||||
>
|
||||
<p
|
||||
className={cn(
|
||||
"font-[family-name:var(--font-title)] leading-snug break-words",
|
||||
size.fontClass,
|
||||
size.clampClass,
|
||||
// Drop-shadow only helps when text sits on imagery; on the
|
||||
// flat bg it just blurs the type.
|
||||
firstImage && "drop-shadow-sm",
|
||||
// Centred quotes need a soft horizontal cap so a long line
|
||||
// doesn't kiss the card edge — looks better with margins.
|
||||
!bottomLeft ? "max-w-[90%]" : "w-full",
|
||||
)}
|
||||
>
|
||||
“{post.caption}”
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 text-[0.65rem] uppercase tracking-[0.2em] font-medium truncate",
|
||||
firstImage ? "text-white/80" : "text-muted-foreground",
|
||||
bottomLeft ? "w-full" : "max-w-[90%]",
|
||||
)}
|
||||
>
|
||||
{displaySourceLabel(post.source_url, post.source_title)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{onUpdated || onDelete ? (
|
||||
<div className="absolute top-2 right-2 flex items-center gap-1.5 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity">
|
||||
{onUpdated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBgModal(true)}
|
||||
aria-label="Update background"
|
||||
title="Update background"
|
||||
className="size-8 rounded-full inline-flex items-center justify-center bg-black/40 text-white hover:bg-black/60 transition-colors"
|
||||
>
|
||||
<ImageIcon className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(post.id)}
|
||||
aria-label="Remove"
|
||||
className="size-8 rounded-full inline-flex items-center justify-center bg-black/40 text-white hover:bg-black/60 transition-colors"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{onUpdated ? (
|
||||
<UpdateBackgroundModal
|
||||
open={bgModal}
|
||||
onClose={() => setBgModal(false)}
|
||||
post={post}
|
||||
isMobile={isMobile}
|
||||
onUpdated={onUpdated}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user