186 lines
6.1 KiB
TypeScript
186 lines
6.1 KiB
TypeScript
import { X } from "lucide-react";
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useI18n } from "../i18n";
|
|
import type { Attachment } from "../types/post";
|
|
|
|
export type SaveToAlbumMediaKind = "image" | "video";
|
|
|
|
type SaveToAlbumGuideContextValue = {
|
|
showSaveToAlbumGuide: (kind: SaveToAlbumMediaKind) => void;
|
|
};
|
|
|
|
const SaveToAlbumGuideContext =
|
|
createContext<SaveToAlbumGuideContextValue | null>(null);
|
|
|
|
export function useSaveToAlbumGuide(): SaveToAlbumGuideContextValue {
|
|
const ctx = useContext(SaveToAlbumGuideContext);
|
|
if (!ctx) {
|
|
throw new Error(
|
|
"useSaveToAlbumGuide must be used within SaveToAlbumGuideProvider",
|
|
);
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function mediaSaveKindFromAttachment(
|
|
attachment: Attachment,
|
|
): SaveToAlbumMediaKind | null {
|
|
if (attachment.kind === "image" || attachment.mime.startsWith("image/")) {
|
|
return "image";
|
|
}
|
|
if (attachment.kind === "video" || attachment.mime.startsWith("video/")) {
|
|
return "video";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function mediaSaveKindFromType(
|
|
type: string | undefined,
|
|
): SaveToAlbumMediaKind | null {
|
|
if (type === "image") return "image";
|
|
if (type === "video") return "video";
|
|
return null;
|
|
}
|
|
|
|
type SavePlatform = "ios" | "android" | "desktop";
|
|
|
|
function detectSavePlatform(): SavePlatform {
|
|
if (typeof navigator === "undefined") return "desktop";
|
|
const ua = navigator.userAgent || "";
|
|
if (/android/i.test(ua)) return "android";
|
|
if (/iPad|iPhone|iPod/i.test(ua)) return "ios";
|
|
if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) {
|
|
return "ios";
|
|
}
|
|
return "desktop";
|
|
}
|
|
|
|
function platformStepKeys(platform: SavePlatform): string[] {
|
|
if (platform === "ios") {
|
|
return [
|
|
"saveAlbumGuideIosStep1",
|
|
"saveAlbumGuideIosStep2",
|
|
"saveAlbumGuideIosStep3",
|
|
];
|
|
}
|
|
if (platform === "android") {
|
|
return [
|
|
"saveAlbumGuideAndroidStep1",
|
|
"saveAlbumGuideAndroidStep2",
|
|
"saveAlbumGuideAndroidStep3",
|
|
];
|
|
}
|
|
return ["saveAlbumGuideDesktopStep1", "saveAlbumGuideDesktopStep2"];
|
|
}
|
|
|
|
export function SaveToAlbumGuideProvider({
|
|
children,
|
|
}: {
|
|
children: ReactNode;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const [mediaKind, setMediaKind] = useState<SaveToAlbumMediaKind | null>(null);
|
|
const platform = useMemo(() => detectSavePlatform(), []);
|
|
|
|
const showSaveToAlbumGuide = useCallback((kind: SaveToAlbumMediaKind) => {
|
|
setMediaKind(kind);
|
|
}, []);
|
|
|
|
const close = useCallback(() => setMediaKind(null), []);
|
|
|
|
useEffect(() => {
|
|
if (!mediaKind) return;
|
|
const onKey = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") close();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [close, mediaKind]);
|
|
|
|
const value = useMemo(
|
|
() => ({ showSaveToAlbumGuide }),
|
|
[showSaveToAlbumGuide],
|
|
);
|
|
|
|
return (
|
|
<SaveToAlbumGuideContext.Provider value={value}>
|
|
{children}
|
|
{mediaKind
|
|
? createPortal(
|
|
<div
|
|
className="fixed inset-0 z-[130] flex items-center justify-center bg-black/70 px-4 backdrop-blur-sm"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="save-album-guide-title"
|
|
onClick={close}
|
|
>
|
|
<div
|
|
className="w-full max-w-md overflow-hidden rounded-3xl border border-white/10 bg-[#1c1c21] text-neutral-100 shadow-2xl shadow-black/70"
|
|
onClick={(event) => event.stopPropagation()}
|
|
>
|
|
<div className="flex items-start justify-between gap-4 border-b border-white/10 px-5 py-4">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ark-gold/80">
|
|
{mediaKind === "video"
|
|
? t("saveAlbumGuideVideoLabel")
|
|
: t("saveAlbumGuideImageLabel")}
|
|
</p>
|
|
<h2
|
|
id="save-album-guide-title"
|
|
className="mt-1 text-lg font-semibold text-white"
|
|
>
|
|
{t("saveAlbumGuideTitle")}
|
|
</h2>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={close}
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
|
aria-label={t("cancel")}
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="px-5 py-5">
|
|
<p className="text-sm leading-6 text-neutral-300">
|
|
{t("saveAlbumGuideIntro")}
|
|
</p>
|
|
<ol className="mt-4 space-y-3">
|
|
{platformStepKeys(platform).map((key, index) => (
|
|
<li key={key} className="flex gap-3">
|
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ark-gold text-sm font-bold text-black">
|
|
{index + 1}
|
|
</span>
|
|
<span className="pt-0.5 text-sm leading-6 text-neutral-100">
|
|
{t(key)}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
<button
|
|
type="button"
|
|
onClick={close}
|
|
className="mt-5 flex h-11 w-full items-center justify-center rounded-full bg-ark-gold px-4 text-sm font-semibold text-black transition hover:bg-ark-gold2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#1c1c21]"
|
|
>
|
|
{t("confirm")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
: null}
|
|
</SaveToAlbumGuideContext.Provider>
|
|
);
|
|
}
|