feat: add responsive case study sharing
This commit is contained in:
@@ -528,6 +528,163 @@
|
|||||||
border-color: rgba(255, 255, 255, 0.16);
|
border-color: rgba(255, 255, 255, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shareDialog {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: none;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-4);
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog:not([open]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog::backdrop {
|
||||||
|
background: rgba(1, 13, 39, 0.62);
|
||||||
|
opacity: 0;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
transition: opacity 180ms var(--motion-ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog[data-state='open']::backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog[data-state='closing']::backdrop {
|
||||||
|
opacity: 0;
|
||||||
|
transition-duration: 140ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePanel {
|
||||||
|
width: min(30rem, 100%);
|
||||||
|
padding: var(--space-5);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
box-shadow: 0 1.5rem 5rem rgba(1, 13, 39, 0.3);
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.97) translateY(0.5rem);
|
||||||
|
transition:
|
||||||
|
opacity 180ms var(--motion-ease-out),
|
||||||
|
transform 180ms var(--motion-ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog[data-state='open'] .sharePanel {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog[data-state='closing'] .sharePanel {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98) translateY(0.25rem);
|
||||||
|
transition-duration: 140ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareDialog[data-motion='none'] .sharePanel,
|
||||||
|
.shareDialog[data-motion='none']::backdrop {
|
||||||
|
transition-duration: 0ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding-bottom: var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareHeader h2 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-h3);
|
||||||
|
line-height: var(--line-height-h3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareHeader p {
|
||||||
|
max-width: 22rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-size-body-small);
|
||||||
|
line-height: var(--line-height-body-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareOptions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-block: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareOptions a {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-2);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-label-small);
|
||||||
|
font-weight: 650;
|
||||||
|
text-align: center;
|
||||||
|
transition:
|
||||||
|
border-color var(--motion-duration-fast) ease,
|
||||||
|
background-color var(--motion-duration-fast) ease,
|
||||||
|
transform var(--motion-duration-fast) var(--motion-ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareOptions svg,
|
||||||
|
.copyLinkButton svg {
|
||||||
|
width: var(--size-icon-md);
|
||||||
|
height: var(--size-icon-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyLinkButton {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 3rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
transition:
|
||||||
|
border-color var(--motion-duration-fast) ease,
|
||||||
|
background-color var(--motion-duration-fast) ease,
|
||||||
|
transform var(--motion-duration-fast) var(--motion-ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
.galleryStage:focus-visible,
|
||||||
|
.thumbnailButton:focus-visible,
|
||||||
|
.viewerThumbnail:focus-visible,
|
||||||
|
.viewerIconButton:focus-visible,
|
||||||
|
.viewerNavButton:focus-visible,
|
||||||
|
.shareOptions a:focus-visible,
|
||||||
|
.copyLinkButton:focus-visible {
|
||||||
|
outline: 3px solid var(--focus-ring, var(--accent-default));
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.galleryStage:active,
|
.galleryStage:active,
|
||||||
.thumbnailButton:active,
|
.thumbnailButton:active,
|
||||||
.viewerThumbnail:active,
|
.viewerThumbnail:active,
|
||||||
@@ -535,6 +692,11 @@
|
|||||||
transform: scale(0.97);
|
transform: scale(0.97);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shareOptions a:active,
|
||||||
|
.copyLinkButton:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
.storyGrid,
|
.storyGrid,
|
||||||
.stackGrid {
|
.stackGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -780,6 +942,14 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding-inline: var(--space-3);
|
padding-inline: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sharePanel {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shareOptions {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
@@ -798,6 +968,12 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shareOptions a:hover,
|
||||||
|
.copyLinkButton:hover {
|
||||||
|
border-color: var(--accent-default);
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
.projectNavLink:hover {
|
.projectNavLink:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
@@ -822,6 +998,16 @@
|
|||||||
transition-duration: 120ms;
|
transition-duration: 120ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sharePanel,
|
||||||
|
.shareDialog::backdrop {
|
||||||
|
transition-duration: 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sharePanel,
|
||||||
|
.shareDialog[data-state='closing'] .sharePanel {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.thumbnailRail {
|
.thumbnailRail {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState, type PointerEvent, type ReactNode } from 'react';
|
import { useCallback, useEffect, useRef, type PointerEvent, type ReactNode } from 'react';
|
||||||
import { Check, Copy, X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { messagesByLocale, type Locale } from '@/i18n/content';
|
import { messagesByLocale, type Locale } from '@/i18n/content';
|
||||||
import { useDismissCaseStudy } from './PortfolioRouteShell';
|
import { useDismissCaseStudy } from './PortfolioRouteShell';
|
||||||
|
import { CaseStudyShare } from './CaseStudyShare';
|
||||||
import { useDrawerDrag } from './useDrawerDrag';
|
import { useDrawerDrag } from './useDrawerDrag';
|
||||||
import styles from './CaseStudyDrawer.module.css';
|
import styles from './CaseStudyDrawer.module.css';
|
||||||
|
|
||||||
type CopyState = 'idle' | 'copied' | 'error';
|
|
||||||
|
|
||||||
export function CaseStudyDrawer({ children, locale }: { children: ReactNode; locale: Locale }) {
|
export function CaseStudyDrawer({ children, locale }: { children: ReactNode; locale: Locale }) {
|
||||||
const copy = messagesByLocale[locale].projects;
|
const copy = messagesByLocale[locale].projects;
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -21,7 +20,6 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
const closing = useRef(false);
|
const closing = useRef(false);
|
||||||
const returnFocusRef = useRef<HTMLElement | null>(null);
|
const returnFocusRef = useRef<HTMLElement | null>(null);
|
||||||
const backgroundScrollY = useRef(0);
|
const backgroundScrollY = useRef(0);
|
||||||
const [copyState, setCopyState] = useState<CopyState>('idle');
|
|
||||||
|
|
||||||
const finishDismiss = useCallback(() => {
|
const finishDismiss = useCallback(() => {
|
||||||
const dialog = dialogRef.current;
|
const dialog = dialogRef.current;
|
||||||
@@ -95,20 +93,10 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollRef.current?.scrollTo({ top: 0, behavior: 'auto' });
|
scrollRef.current?.scrollTo({ top: 0, behavior: 'auto' });
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setCopyState('idle');
|
|
||||||
dialogRef.current?.querySelector<HTMLElement>('[data-case-study-initial-focus]')?.focus();
|
dialogRef.current?.querySelector<HTMLElement>('[data-case-study-initial-focus]')?.focus();
|
||||||
});
|
});
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const copyCurrentLink = async () => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(window.location.href);
|
|
||||||
setCopyState('copied');
|
|
||||||
} catch {
|
|
||||||
setCopyState('error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBackdropDown = (event: PointerEvent<HTMLDialogElement>) => {
|
const handleBackdropDown = (event: PointerEvent<HTMLDialogElement>) => {
|
||||||
backdropPressed.current = event.target === event.currentTarget;
|
backdropPressed.current = event.target === event.currentTarget;
|
||||||
};
|
};
|
||||||
@@ -118,8 +106,6 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
backdropPressed.current = false;
|
backdropPressed.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const feedback = copyState === 'copied' ? copy.linkCopied : copyState === 'error' ? copy.copyFailed : '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog
|
<dialog
|
||||||
ref={dialogRef}
|
ref={dialogRef}
|
||||||
@@ -141,10 +127,7 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
</div>
|
</div>
|
||||||
<span className={styles.drawerLabel}>{copy.drawerLabel}</span>
|
<span className={styles.drawerLabel}>{copy.drawerLabel}</span>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<button type="button" className={styles.actionButton} onClick={copyCurrentLink} aria-label={copy.copyLink}>
|
<CaseStudyShare locale={locale} />
|
||||||
{copyState === 'copied' ? <Check size={19} aria-hidden="true" /> : <Copy size={18} aria-hidden="true" />}
|
|
||||||
<span className={styles.actionText}>{copyState === 'copied' ? copy.linkCopied : copy.copyLink}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.iconButton}
|
className={styles.iconButton}
|
||||||
@@ -156,7 +139,6 @@ export function CaseStudyDrawer({ children, locale }: { children: ReactNode; loc
|
|||||||
<X size={21} aria-hidden="true" />
|
<X size={21} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.liveRegion} aria-live="polite">{feedback}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div ref={scrollRef} className={styles.scrollArea}>{children}</div>
|
<div ref={scrollRef} className={styles.scrollArea}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
171
src/components/CaseStudyShare.tsx
Normal file
171
src/components/CaseStudyShare.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
Linkedin,
|
||||||
|
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">
|
||||||
|
<Linkedin 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,6 +55,15 @@ export interface SiteMessages {
|
|||||||
liveProjectLabel: (title: string) => string;
|
liveProjectLabel: (title: string) => string;
|
||||||
drawerLabel: string;
|
drawerLabel: string;
|
||||||
closeCaseStudy: string;
|
closeCaseStudy: string;
|
||||||
|
shareLabel: string;
|
||||||
|
shareCaseStudy: string;
|
||||||
|
shareDialogTitle: string;
|
||||||
|
shareDialogDescription: string;
|
||||||
|
closeShareDialog: string;
|
||||||
|
shareTelegram: string;
|
||||||
|
shareWhatsApp: string;
|
||||||
|
shareLinkedIn: string;
|
||||||
|
shareEmail: string;
|
||||||
copyLink: string;
|
copyLink: string;
|
||||||
linkCopied: string;
|
linkCopied: string;
|
||||||
copyFailed: string;
|
copyFailed: string;
|
||||||
@@ -206,6 +215,15 @@ export const messagesByLocale = {
|
|||||||
liveProjectLabel: (title) => `Open the live ${title} project in a new tab`,
|
liveProjectLabel: (title) => `Open the live ${title} project in a new tab`,
|
||||||
drawerLabel: 'Project case study',
|
drawerLabel: 'Project case study',
|
||||||
closeCaseStudy: 'Close case study',
|
closeCaseStudy: 'Close case study',
|
||||||
|
shareLabel: 'Share',
|
||||||
|
shareCaseStudy: 'Share this case study',
|
||||||
|
shareDialogTitle: 'Share case study',
|
||||||
|
shareDialogDescription: 'Send this project to someone or copy its direct link.',
|
||||||
|
closeShareDialog: 'Close sharing options',
|
||||||
|
shareTelegram: 'Telegram',
|
||||||
|
shareWhatsApp: 'WhatsApp',
|
||||||
|
shareLinkedIn: 'LinkedIn',
|
||||||
|
shareEmail: 'Email',
|
||||||
copyLink: 'Copy link',
|
copyLink: 'Copy link',
|
||||||
linkCopied: 'Link copied',
|
linkCopied: 'Link copied',
|
||||||
copyFailed: 'Could not copy the link',
|
copyFailed: 'Could not copy the link',
|
||||||
@@ -358,6 +376,15 @@ export const messagesByLocale = {
|
|||||||
liveProjectLabel: (title) => `باز کردن نسخه زنده ${title} در زبانه جدید`,
|
liveProjectLabel: (title) => `باز کردن نسخه زنده ${title} در زبانه جدید`,
|
||||||
drawerLabel: 'مطالعه موردی پروژه',
|
drawerLabel: 'مطالعه موردی پروژه',
|
||||||
closeCaseStudy: 'بستن مطالعه موردی',
|
closeCaseStudy: 'بستن مطالعه موردی',
|
||||||
|
shareLabel: 'اشتراکگذاری',
|
||||||
|
shareCaseStudy: 'اشتراکگذاری این مطالعه موردی',
|
||||||
|
shareDialogTitle: 'اشتراکگذاری مطالعه موردی',
|
||||||
|
shareDialogDescription: 'این پروژه را برای دیگران بفرستید یا پیوند مستقیم آن را کپی کنید.',
|
||||||
|
closeShareDialog: 'بستن گزینههای اشتراکگذاری',
|
||||||
|
shareTelegram: 'تلگرام',
|
||||||
|
shareWhatsApp: 'واتساپ',
|
||||||
|
shareLinkedIn: 'لینکدین',
|
||||||
|
shareEmail: 'ایمیل',
|
||||||
copyLink: 'کپی پیوند',
|
copyLink: 'کپی پیوند',
|
||||||
linkCopied: 'پیوند کپی شد',
|
linkCopied: 'پیوند کپی شد',
|
||||||
copyFailed: 'کپی پیوند ممکن نشد',
|
copyFailed: 'کپی پیوند ممکن نشد',
|
||||||
|
|||||||
Reference in New Issue
Block a user