feat: copy caption in post actions menu
This commit is contained in:
+1
-1
@@ -26,7 +26,7 @@ export default function HomePage() {
|
|||||||
const authed = !loading && !!user;
|
const authed = !loading && !!user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 max-w-2xl w-full mx-auto px-4 sm:px-6 py-16 sm:py-24">
|
<main className="flex-1 max-w-2xl w-full mx-auto px-4 sm:px-6 py-16 sm:py-24 select-none">
|
||||||
<div className="flex flex-col items-center text-center gap-5">
|
<div className="flex flex-col items-center text-center gap-5">
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export default function ChantPage() {
|
|||||||
const pct = target > 0 ? Math.min(100, (count / target) * 100) : 0;
|
const pct = target > 0 ? Math.min(100, (count / target) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 w-full max-w-lg mx-auto px-4 sm:px-6 py-5 flex flex-col">
|
<main className="flex-1 w-full max-w-lg mx-auto px-4 sm:px-6 py-5 flex flex-col select-none">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
href="/sadhna-streak"
|
href="/sadhna-streak"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function SadhnaStreakPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 w-full max-w-2xl mx-auto px-4 sm:px-6 py-5 flex flex-col gap-5">
|
<main className="flex-1 w-full max-w-2xl mx-auto px-4 sm:px-6 py-5 flex flex-col gap-5 select-none">
|
||||||
<header className="text-center space-y-0.5">
|
<header className="text-center space-y-0.5">
|
||||||
<h1 className="font-(family-name:--font-title) text-2xl sm:text-3xl font-semibold text-foreground">
|
<h1 className="font-(family-name:--font-title) text-2xl sm:text-3xl font-semibold text-foreground">
|
||||||
Daily Sādhana
|
Daily Sādhana
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function RewardCelebration({
|
|||||||
initial={{ scale: 0.85, opacity: 0, y: 16 }}
|
initial={{ scale: 0.85, opacity: 0, y: 16 }}
|
||||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
transition={{ type: "spring", stiffness: 220, damping: 20 }}
|
transition={{ type: "spring", stiffness: 220, damping: 20 }}
|
||||||
className="relative z-10 w-full max-w-md max-h-[90vh] overflow-y-auto rounded-3xl border border-border bg-card p-6 text-center shadow-2xl"
|
className="relative z-10 w-full max-w-md max-h-[90vh] overflow-y-auto rounded-3xl border border-border bg-card p-6 text-center shadow-2xl select-none"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
|
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
|
||||||
A gift for your sādhana
|
A gift for your sādhana
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ function RewardDetail({
|
|||||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-10 w-full max-w-md max-h-[90vh] overflow-y-auto rounded-3xl border border-border bg-card p-6 text-center shadow-2xl">
|
<div className="relative z-10 w-full max-w-md max-h-[90vh] overflow-y-auto rounded-3xl border border-border bg-card p-6 text-center shadow-2xl select-none">
|
||||||
<div className="relative mx-auto mb-4 flex size-40 items-center justify-center">
|
<div className="relative mx-auto mb-4 flex size-40 items-center justify-center">
|
||||||
<div className="absolute inset-0 rounded-full bg-primary/10 blur-2xl" />
|
<div className="absolute inset-0 rounded-full bg-primary/10 blur-2xl" />
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Ban, Flag, MoreVertical, Trash2 } from "lucide-react";
|
import { Ban, Copy, Flag, MoreVertical, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -41,6 +41,8 @@ import { useAuthGate } from "@/components/auth/auth-gate";
|
|||||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||||
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
import { ConfirmModal } from "@/components/ui/confirm-modal";
|
||||||
import { ReportDialog, type ReportOption } from "@/components/social/report-dialog";
|
import { ReportDialog, type ReportOption } from "@/components/social/report-dialog";
|
||||||
|
import { createShortLink } from "@/lib/share";
|
||||||
|
import { structuredReferenceForPost } from "@/lib/source-reference";
|
||||||
|
|
||||||
// Post report categories — mirrors the BFF allow-list (postReportCategories).
|
// Post report categories — mirrors the BFF allow-list (postReportCategories).
|
||||||
const POST_REPORT_OPTIONS: ReportOption[] = [
|
const POST_REPORT_OPTIONS: ReportOption[] = [
|
||||||
@@ -147,10 +149,45 @@ export function PostCardMenu({
|
|||||||
};
|
};
|
||||||
}, [open, isMobile]);
|
}, [open, isMobile]);
|
||||||
|
|
||||||
|
// Copy the post's caption + source citation + a (short) link to the post —
|
||||||
|
// the same payload the share sheet / footer produce. Available to every
|
||||||
|
// viewer.
|
||||||
|
async function copyCaption() {
|
||||||
|
setOpen(false);
|
||||||
|
try {
|
||||||
|
const origin = window.location.origin;
|
||||||
|
let url = "";
|
||||||
|
if (authorHandle) {
|
||||||
|
const longPath = `/u/${encodeURIComponent(authorHandle)}/posts#post-${post.id}`;
|
||||||
|
const code = await createShortLink(longPath);
|
||||||
|
url = code
|
||||||
|
? new URL(`/share/${code}`, origin).toString()
|
||||||
|
: new URL(longPath, origin).toString();
|
||||||
|
}
|
||||||
|
const ref = structuredReferenceForPost(post.source_url, post.source_title);
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (post.caption) lines.push(`“${post.caption}”`, "");
|
||||||
|
lines.push("Source:", ref.title);
|
||||||
|
if (ref.detail) lines.push(ref.detail);
|
||||||
|
if (url) lines.push(url);
|
||||||
|
await navigator.clipboard.writeText(lines.join("\n"));
|
||||||
|
toast.success("Caption copied");
|
||||||
|
} catch {
|
||||||
|
toast.error("Could not copy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build the items list AFTER all hooks have run so a conditional
|
// Build the items list AFTER all hooks have run so a conditional
|
||||||
// early-return doesn't break the rules-of-hooks contract on
|
// early-return doesn't break the rules-of-hooks contract on
|
||||||
// re-renders where viewer state changes.
|
// re-renders where viewer state changes.
|
||||||
const items: MenuItem[] = [];
|
const items: MenuItem[] = [];
|
||||||
|
// Copy caption — first item for everyone (owner and non-owner alike).
|
||||||
|
items.push({
|
||||||
|
key: "copy",
|
||||||
|
label: "Copy caption",
|
||||||
|
icon: <Copy className="size-4" />,
|
||||||
|
onClick: copyCaption,
|
||||||
|
});
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
if (onDelete) {
|
if (onDelete) {
|
||||||
items.push({
|
items.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user