172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
BriefcaseBusiness,
|
|
Check,
|
|
Copy,
|
|
Mail,
|
|
MessageCircle,
|
|
Send,
|
|
Share2,
|
|
X,
|
|
} from 'lucide-react';
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
type MouseEvent,
|
|
} from 'react';
|
|
import { messagesByLocale, type Locale } from '@/i18n/content';
|
|
import styles from './CaseStudyDrawer.module.css';
|
|
|
|
type CopyState = 'idle' | 'copied' | 'error';
|
|
|
|
export function CaseStudyShare({ locale }: { locale: Locale }) {
|
|
const copy = messagesByLocale[locale].projects;
|
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
const closeRef = useRef<HTMLButtonElement>(null);
|
|
const skipMotionRef = useRef(false);
|
|
const [fallbackOpen, setFallbackOpen] = useState(false);
|
|
const [copyState, setCopyState] = useState<CopyState>('idle');
|
|
const [shareData, setShareData] = useState({ title: '', url: '' });
|
|
|
|
useEffect(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog) return;
|
|
|
|
if (fallbackOpen && !dialog.open) {
|
|
dialog.dataset.motion = skipMotionRef.current ? 'none' : 'standard';
|
|
dialog.showModal();
|
|
requestAnimationFrame(() => {
|
|
dialog.dataset.state = 'open';
|
|
closeRef.current?.focus();
|
|
});
|
|
}
|
|
|
|
if (!fallbackOpen && dialog.open) dialog.close();
|
|
}, [fallbackOpen]);
|
|
|
|
const openFallback = useCallback((skipMotion: boolean) => {
|
|
skipMotionRef.current = skipMotion;
|
|
setCopyState('idle');
|
|
setShareData({ title: document.title, url: window.location.href });
|
|
setFallbackOpen(true);
|
|
}, []);
|
|
|
|
const closeFallback = useCallback(() => {
|
|
const dialog = dialogRef.current;
|
|
if (!dialog?.open) return;
|
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
if (reduceMotion || dialog.dataset.motion === 'none') {
|
|
dialog.close();
|
|
setFallbackOpen(false);
|
|
triggerRef.current?.focus({ preventScroll: true });
|
|
return;
|
|
}
|
|
|
|
dialog.dataset.state = 'closing';
|
|
window.setTimeout(() => {
|
|
dialog.close();
|
|
setFallbackOpen(false);
|
|
triggerRef.current?.focus({ preventScroll: true });
|
|
}, 140);
|
|
}, []);
|
|
|
|
const handleShare = async (event: MouseEvent<HTMLButtonElement>) => {
|
|
const data = { title: document.title, url: window.location.href };
|
|
const useNativeShare = window.matchMedia('(max-width: 1100px)').matches
|
|
&& typeof navigator.share === 'function';
|
|
|
|
if (useNativeShare) {
|
|
try {
|
|
await navigator.share(data);
|
|
return;
|
|
} catch (error) {
|
|
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
}
|
|
}
|
|
|
|
openFallback(event.detail === 0);
|
|
};
|
|
|
|
const copyCurrentLink = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(shareData.url);
|
|
setCopyState('copied');
|
|
} catch {
|
|
setCopyState('error');
|
|
}
|
|
};
|
|
|
|
const encodedUrl = encodeURIComponent(shareData.url);
|
|
const encodedTitle = encodeURIComponent(shareData.title);
|
|
const encodedMessage = encodeURIComponent(`${shareData.title} ${shareData.url}`);
|
|
const feedback = copyState === 'copied' ? copy.linkCopied : copyState === 'error' ? copy.copyFailed : '';
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
ref={triggerRef}
|
|
type="button"
|
|
className={styles.actionButton}
|
|
onClick={handleShare}
|
|
aria-label={copy.shareCaseStudy}
|
|
aria-haspopup="dialog"
|
|
>
|
|
<Share2 size={19} aria-hidden="true" />
|
|
<span className={styles.actionText}>{copy.shareLabel}</span>
|
|
</button>
|
|
|
|
<dialog
|
|
ref={dialogRef}
|
|
className={styles.shareDialog}
|
|
aria-labelledby="case-study-share-title"
|
|
aria-describedby="case-study-share-description"
|
|
data-state="opening"
|
|
onCancel={(event) => {
|
|
event.preventDefault();
|
|
closeFallback();
|
|
}}
|
|
onClick={(event) => {
|
|
if (event.target === event.currentTarget) closeFallback();
|
|
}}
|
|
>
|
|
<section className={styles.sharePanel} onClick={(event) => event.stopPropagation()}>
|
|
<header className={styles.shareHeader}>
|
|
<div>
|
|
<h2 id="case-study-share-title">{copy.shareDialogTitle}</h2>
|
|
<p id="case-study-share-description">{copy.shareDialogDescription}</p>
|
|
</div>
|
|
<button ref={closeRef} type="button" className={styles.iconButton} onClick={closeFallback} aria-label={copy.closeShareDialog}>
|
|
<X size={21} aria-hidden="true" />
|
|
</button>
|
|
</header>
|
|
|
|
<div className={styles.shareOptions}>
|
|
<a href={`https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}`} target="_blank" rel="noopener noreferrer">
|
|
<Send aria-hidden="true" /><span>{copy.shareTelegram}</span>
|
|
</a>
|
|
<a href={`https://wa.me/?text=${encodedMessage}`} target="_blank" rel="noopener noreferrer">
|
|
<MessageCircle aria-hidden="true" /><span>{copy.shareWhatsApp}</span>
|
|
</a>
|
|
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`} target="_blank" rel="noopener noreferrer">
|
|
<BriefcaseBusiness aria-hidden="true" /><span>{copy.shareLinkedIn}</span>
|
|
</a>
|
|
<a href={`mailto:?subject=${encodedTitle}&body=${encodedMessage}`}>
|
|
<Mail aria-hidden="true" /><span>{copy.shareEmail}</span>
|
|
</a>
|
|
</div>
|
|
|
|
<button type="button" className={styles.copyLinkButton} onClick={copyCurrentLink}>
|
|
{copyState === 'copied' ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
|
|
<span>{copyState === 'copied' ? copy.linkCopied : copy.copyLink}</span>
|
|
</button>
|
|
<span className={styles.liveRegion} aria-live="polite">{feedback}</span>
|
|
</section>
|
|
</dialog>
|
|
</>
|
|
);
|
|
}
|