velxio/frontend/src/components/layout/ShareModal.tsx

245 lines
6.7 KiB
TypeScript

/**
* ShareModal — shows a shareable project link and visibility toggle.
*/
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { useProjectStore } from '../../store/useProjectStore';
import { updateProject } from '../../services/projectService';
interface ShareModalProps {
onClose: () => void;
}
export const ShareModal: React.FC<ShareModalProps> = ({ onClose }) => {
const { t } = useTranslation();
const currentProject = useProjectStore((s) => s.currentProject);
const setVisibility = useProjectStore((s) => s.setVisibility);
const [copied, setCopied] = useState(false);
const [toggling, setToggling] = useState(false);
if (!currentProject) return null;
const shareUrl = `${window.location.origin}/project/${currentProject.id}`;
const isPublic = currentProject.isPublic;
const handleCopy = () => {
navigator.clipboard.writeText(shareUrl).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const handleToggleVisibility = async () => {
setToggling(true);
try {
await updateProject(currentProject.id, { is_public: !isPublic });
setVisibility(!isPublic);
} catch {
// Silently fail — user can retry
} finally {
setToggling(false);
}
};
return createPortal(
<div style={styles.overlay} onClick={onClose}>
<div style={styles.modal} onClick={(e) => e.stopPropagation()}>
<h2 style={styles.title}>{t('editor.share.title')}</h2>
{/* Visibility toggle */}
<div style={styles.visibilityRow}>
<div style={styles.visibilityInfo}>
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
{isPublic ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#4ade80"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#f59e0b"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
)}
<span
style={{ color: isPublic ? '#4ade80' : '#f59e0b', fontWeight: 600, fontSize: 13 }}
>
{isPublic ? t('editor.share.public') : t('editor.share.private')}
</span>
</span>
<span style={{ color: '#888', fontSize: 12 }}>
{isPublic ? t('editor.share.publicHint') : t('editor.share.privateHint')}
</span>
</div>
<button
onClick={handleToggleVisibility}
disabled={toggling}
style={{
...styles.toggleBtn,
opacity: toggling ? 0.5 : 1,
}}
>
{toggling
? '...'
: isPublic
? t('editor.share.makePrivate')
: t('editor.share.makePublic')}
</button>
</div>
{/* Share link */}
<div style={styles.linkRow}>
<input
type="text"
value={shareUrl}
readOnly
style={styles.linkInput}
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button onClick={handleCopy} style={styles.copyBtn}>
{copied ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#4ade80"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
) : (
t('editor.share.copy')
)}
</button>
</div>
{!isPublic && (
<div style={styles.warning}>{t('editor.share.privateWarning')}</div>
)}
<div style={styles.actions}>
<button onClick={onClose} style={styles.closeBtn}>
{t('editor.share.close')}
</button>
</div>
</div>
</div>,
document.body,
);
};
const styles: Record<string, React.CSSProperties> = {
overlay: {
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,.6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
},
modal: {
background: '#252526',
border: '1px solid #3c3c3c',
borderRadius: 8,
padding: '1.75rem',
width: 440,
display: 'flex',
flexDirection: 'column',
gap: 16,
},
title: { color: '#ccc', margin: 0, fontSize: 18, fontWeight: 600 },
visibilityRow: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
padding: '10px 12px',
background: '#1e1e1e',
border: '1px solid #333',
borderRadius: 6,
},
visibilityInfo: {
display: 'flex',
flexDirection: 'column',
gap: 4,
},
toggleBtn: {
background: 'transparent',
border: '1px solid #555',
borderRadius: 4,
color: '#ccc',
padding: '6px 12px',
fontSize: 12,
cursor: 'pointer',
whiteSpace: 'nowrap',
flexShrink: 0,
},
linkRow: { display: 'flex', gap: 6 },
linkInput: {
flex: 1,
background: '#1e1e1e',
border: '1px solid #444',
borderRadius: 4,
padding: '8px 10px',
color: '#4fc3f7',
fontSize: 13,
fontFamily: 'monospace',
outline: 'none',
},
copyBtn: {
background: '#0e639c',
border: 'none',
borderRadius: 4,
color: '#fff',
padding: '8px 16px',
fontSize: 13,
cursor: 'pointer',
fontWeight: 500,
display: 'flex',
alignItems: 'center',
},
warning: {
background: '#3d2e00',
border: '1px solid #f59e0b44',
borderRadius: 4,
color: '#f59e0b',
padding: '8px 12px',
fontSize: 12,
},
actions: { display: 'flex', justifyContent: 'flex-end' },
closeBtn: {
background: 'transparent',
border: '1px solid #555',
borderRadius: 4,
color: '#ccc',
padding: '8px 16px',
fontSize: 13,
cursor: 'pointer',
},
};