fix: download button on cards opens the post page
On compact cards the user cannot see which file is attached, especially for multi-attachment posts. Make the small download button on PopularRankRow, RecommendedCard, and LatestUpdateCard navigate to the post detail page instead of triggering an immediate download, so the user lands in the full post and picks the exact attachment to download.
This commit is contained in:
@@ -1,17 +1,10 @@
|
|||||||
import { LoaderCircle, Play } from "lucide-react";
|
import { Play } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { FavoriteButton } from "../favorites/FavoriteButton";
|
import { FavoriteButton } from "../favorites/FavoriteButton";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { useLocalizedPath } from "../useLocalizedPath";
|
import { useLocalizedPath } from "../useLocalizedPath";
|
||||||
import type { Attachment, Post } from "../types/post";
|
import type { Attachment, Post } from "../types/post";
|
||||||
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
|
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
|
||||||
import {
|
|
||||||
mediaSaveKindFromAttachment,
|
|
||||||
useSaveToAlbumGuide,
|
|
||||||
} from "./SaveToAlbumGuide";
|
|
||||||
import { useToast } from "./Toast";
|
|
||||||
import { downloadAttachment } from "./messageStream/utils/downloadFile";
|
|
||||||
import { fileIcon } from "./messageStream/utils/fileIcon";
|
import { fileIcon } from "./messageStream/utils/fileIcon";
|
||||||
import {
|
import {
|
||||||
filenameWithExtension,
|
filenameWithExtension,
|
||||||
@@ -30,24 +23,11 @@ function LatestActions({
|
|||||||
attachment?: Attachment;
|
attachment?: Attachment;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
const lp = useLocalizedPath();
|
||||||
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
const navigate = useNavigate();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const goToPost = () => {
|
||||||
if (!attachment || isDownloading) return;
|
navigate(lp(`/browse?post=${encodeURIComponent(post.id)}`));
|
||||||
setIsDownloading(true);
|
|
||||||
try {
|
|
||||||
await downloadAttachment(post.id, attachment.id, attachment.filename, {
|
|
||||||
sizeBytes: attachment.sizeBytes,
|
|
||||||
});
|
|
||||||
const mediaKind = mediaSaveKindFromAttachment(attachment);
|
|
||||||
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
|
||||||
} catch {
|
|
||||||
showToast(t("downloadFail"), "error");
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,23 +39,13 @@ function LatestActions({
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
void handleDownload();
|
goToPost();
|
||||||
}}
|
}}
|
||||||
disabled={isDownloading}
|
aria-label={t("download")}
|
||||||
aria-label={
|
title={t("download")}
|
||||||
isDownloading ? t("downloading") : `Download ${attachment.filename}`
|
className="relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70"
|
||||||
}
|
|
||||||
aria-busy={isDownloading}
|
|
||||||
className="relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70 disabled:cursor-wait"
|
|
||||||
>
|
>
|
||||||
{isDownloading ? (
|
<DownloadCloudIcon className="h-5 w-5" />
|
||||||
<LoaderCircle
|
|
||||||
className="h-4 w-4 animate-spin text-[#A8A9AE]"
|
|
||||||
strokeWidth={2.3}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DownloadCloudIcon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
Link as LinkIcon,
|
Link as LinkIcon,
|
||||||
LoaderCircle,
|
|
||||||
Music,
|
Music,
|
||||||
Presentation,
|
Presentation,
|
||||||
Video,
|
Video,
|
||||||
@@ -21,9 +20,6 @@ import { cleanCategoryDisplayName } from "../utils/categoryDisplay";
|
|||||||
import { formatDateYmd } from "../utils/format";
|
import { formatDateYmd } from "../utils/format";
|
||||||
import { postToResource } from "../utils/postResourceAdapter";
|
import { postToResource } from "../utils/postResourceAdapter";
|
||||||
import type { Post } from "../types/post";
|
import type { Post } from "../types/post";
|
||||||
import { downloadAttachment } from "./messageStream/utils/downloadFile";
|
|
||||||
import { mediaSaveKindFromType, useSaveToAlbumGuide } from "./SaveToAlbumGuide";
|
|
||||||
import { useToast } from "./Toast";
|
|
||||||
import { FavoriteButton } from "../favorites/FavoriteButton";
|
import { FavoriteButton } from "../favorites/FavoriteButton";
|
||||||
|
|
||||||
const MEDALS = ["🥇", "🥈", "🥉"];
|
const MEDALS = ["🥇", "🥈", "🥉"];
|
||||||
@@ -105,31 +101,18 @@ export function PopularRankRow({
|
|||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const lp = useLocalizedPath();
|
const lp = useLocalizedPath();
|
||||||
const { showToast } = useToast();
|
|
||||||
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
const [coverFailed, setCoverFailed] = useState(false);
|
const [coverFailed, setCoverFailed] = useState(false);
|
||||||
|
|
||||||
const r = postToResource(post, lang, categories);
|
const r = postToResource(post, lang, categories);
|
||||||
const cover = r.coverImage && !coverFailed ? assetUrl(r.coverImage) : "";
|
const cover = r.coverImage && !coverFailed ? assetUrl(r.coverImage) : "";
|
||||||
const isTop3 = showRank && index < MEDALS.length;
|
const isTop3 = showRank && index < MEDALS.length;
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const goToPost = () => {
|
||||||
if (isDownloading || !r.downloadPostId || !r.downloadAttachmentId) return;
|
const params = new URLSearchParams();
|
||||||
setIsDownloading(true);
|
if (browseSort) params.set("sort", browseSort);
|
||||||
try {
|
params.set("post", post.id);
|
||||||
await downloadAttachment(
|
if (singlePostLink) params.set("single", "1");
|
||||||
r.downloadPostId,
|
navigate(lp(`/browse?${params.toString()}`));
|
||||||
r.downloadAttachmentId,
|
|
||||||
r.title,
|
|
||||||
);
|
|
||||||
const mediaKind = mediaSaveKindFromType(r.type);
|
|
||||||
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
|
||||||
} catch {
|
|
||||||
showToast(t("downloadFail"), "error");
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,13 +125,7 @@ export function PopularRankRow({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={goToPost}
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (browseSort) params.set("sort", browseSort);
|
|
||||||
params.set("post", post.id);
|
|
||||||
if (singlePostLink) params.set("single", "1");
|
|
||||||
navigate(lp(`/browse?${params.toString()}`));
|
|
||||||
}}
|
|
||||||
aria-label={r.title}
|
aria-label={r.title}
|
||||||
className="absolute inset-0 z-0 rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/70"
|
className="absolute inset-0 z-0 rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/70"
|
||||||
/>
|
/>
|
||||||
@@ -216,20 +193,16 @@ export function PopularRankRow({
|
|||||||
{showDownload && r.isDownloadable ? (
|
{showDownload && r.isDownloadable ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
onClick={(event) => {
|
||||||
disabled={isDownloading}
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
goToPost();
|
||||||
|
}}
|
||||||
aria-label={t("download")}
|
aria-label={t("download")}
|
||||||
title={t("download")}
|
title={t("download")}
|
||||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70 disabled:cursor-wait"
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/70"
|
||||||
>
|
>
|
||||||
{isDownloading ? (
|
<DownloadCloudIcon className="h-5 w-5" />
|
||||||
<LoaderCircle
|
|
||||||
className="h-4 w-4 animate-spin"
|
|
||||||
strokeWidth={2.3}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DownloadCloudIcon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { Download, LoaderCircle } from "lucide-react";
|
|
||||||
import { m } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import type { Resource } from "../api";
|
import type { Resource } from "../api";
|
||||||
import { assetUrl } from "../api";
|
import { assetUrl } from "../api";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { useLocalizedPath } from "../useLocalizedPath";
|
import { useLocalizedPath } from "../useLocalizedPath";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { formatDateYmd } from "../utils/format";
|
import { formatDateYmd } from "../utils/format";
|
||||||
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
|
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
|
||||||
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
||||||
import {
|
|
||||||
downloadAttachment,
|
|
||||||
downloadFile,
|
|
||||||
} from "./messageStream/utils/downloadFile";
|
|
||||||
import { mediaSaveKindFromType, useSaveToAlbumGuide } from "./SaveToAlbumGuide";
|
|
||||||
import { useToast } from "./Toast";
|
|
||||||
import { FavoriteButton } from "../favorites/FavoriteButton";
|
import { FavoriteButton } from "../favorites/FavoriteButton";
|
||||||
|
|
||||||
function isPlaceholderAsset(path: string | undefined | null) {
|
function isPlaceholderAsset(path: string | undefined | null) {
|
||||||
@@ -53,9 +46,7 @@ export function RecommendedCard({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const lp = useLocalizedPath();
|
const lp = useLocalizedPath();
|
||||||
const { showToast } = useToast();
|
const navigate = useNavigate();
|
||||||
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
const figmaCover =
|
const figmaCover =
|
||||||
officialRecommendationCoverFallbacks[
|
officialRecommendationCoverFallbacks[
|
||||||
visualIndex % officialRecommendationCoverFallbacks.length
|
visualIndex % officialRecommendationCoverFallbacks.length
|
||||||
@@ -77,26 +68,10 @@ export function RecommendedCard({
|
|||||||
? assetUrl(r.fileUrl || r.previewUrl)
|
? assetUrl(r.fileUrl || r.previewUrl)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const goToPost = () => {
|
||||||
if (isDownloading) return;
|
// Same destination as the card-wide overlay link, so the user lands on the
|
||||||
setIsDownloading(true);
|
// post and can choose exactly which attachment to download.
|
||||||
try {
|
navigate(lp(`/resource/${r.id}`));
|
||||||
if (r.downloadPostId && r.downloadAttachmentId) {
|
|
||||||
await downloadAttachment(
|
|
||||||
r.downloadPostId,
|
|
||||||
r.downloadAttachmentId,
|
|
||||||
displayTitle,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await downloadFile(dl, displayTitle);
|
|
||||||
}
|
|
||||||
const mediaKind = mediaSaveKindFromType(r.type);
|
|
||||||
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
|
||||||
} catch {
|
|
||||||
showToast(t("downloadFail"), "error");
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -193,29 +168,18 @@ export function RecommendedCard({
|
|||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
useFigmaDesign
|
useFigmaDesign
|
||||||
? "relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
|
? "relative z-20 flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-[#191921] text-white outline-none transition hover:bg-[#22232D] active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
||||||
: "relative z-20 shrink-0 rounded-lg p-1 text-white outline-none transition hover:bg-ark-gold/10 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
|
: "relative z-20 shrink-0 rounded-lg p-1 text-white outline-none transition hover:bg-ark-gold/10 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
||||||
}
|
}
|
||||||
title={isDownloading ? t("downloading") : t("download")}
|
title={t("download")}
|
||||||
aria-label={isDownloading ? t("downloading") : t("download")}
|
aria-label={t("download")}
|
||||||
aria-busy={isDownloading}
|
|
||||||
disabled={isDownloading}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
void handleDownload();
|
goToPost();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDownloading ? (
|
<DownloadCloudIcon className="h-5 w-5" />
|
||||||
<LoaderCircle
|
|
||||||
className="h-4 w-4 animate-spin"
|
|
||||||
strokeWidth={2.2}
|
|
||||||
/>
|
|
||||||
) : useFigmaDesign ? (
|
|
||||||
<DownloadCloudIcon className="h-5 w-5" />
|
|
||||||
) : (
|
|
||||||
<Download className="h-5 w-5" strokeWidth={2.2} />
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user