mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: include iframe pop-ups on WebPreviews
This commit is contained in:
parent
89889a93bf
commit
35b96c4473
2 changed files with 174 additions and 95 deletions
|
|
@ -3,6 +3,11 @@ import { NodeViewWrapper } from '@tiptap/react';
|
|||
import { Globe, Edit2, Save, X, AlignLeft, AlignCenter, AlignRight, Trash } from 'lucide-react';
|
||||
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext';
|
||||
import { getUrlPreview } from '@services/courses/activities';
|
||||
import Modal from '@components/Objects/StyledElements/Modal/Modal';
|
||||
import { Input } from '@components/ui/input';
|
||||
import { Label } from '@components/ui/label';
|
||||
import { Checkbox } from '@components/ui/checkbox';
|
||||
import { Button } from '@components/ui/button';
|
||||
|
||||
interface EditorContext {
|
||||
isEditable: boolean;
|
||||
|
|
@ -46,6 +51,9 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
|
||||
const [buttonLabel, setButtonLabel] = useState(node.attrs.buttonLabel || 'Visit Site');
|
||||
const [showButton, setShowButton] = useState(node.attrs.showButton !== false);
|
||||
const [openInPopup, setOpenInPopup] = useState(node.attrs.openInPopup || false);
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(!node.attrs.url);
|
||||
|
||||
const fetchPreview = async (url: string) => {
|
||||
setLoading(true);
|
||||
|
|
@ -79,7 +87,15 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
useEffect(() => {
|
||||
setButtonLabel(node.attrs.buttonLabel || 'Visit Site');
|
||||
setShowButton(!!node.attrs.showButton);
|
||||
}, [node.attrs.buttonLabel, node.attrs.showButton]);
|
||||
setOpenInPopup(!!node.attrs.openInPopup);
|
||||
}, [node.attrs.buttonLabel, node.attrs.showButton, node.attrs.openInPopup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!node.attrs.url) {
|
||||
setEditing(true);
|
||||
setModalOpen(true);
|
||||
}
|
||||
}, [node.attrs.url]);
|
||||
|
||||
const handleAlignmentChange = (value: string) => {
|
||||
updateAttributes({ alignment: value });
|
||||
|
|
@ -88,6 +104,7 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
const handleEdit = () => {
|
||||
setEditing(true);
|
||||
setInputUrl(node.attrs.url || '');
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveEdit = () => {
|
||||
|
|
@ -95,14 +112,17 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
fetchPreview(inputUrl);
|
||||
} else {
|
||||
setEditing(false);
|
||||
setModalOpen(false);
|
||||
}
|
||||
updateAttributes({ buttonLabel, showButton });
|
||||
updateAttributes({ buttonLabel, showButton, openInPopup });
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditing(false);
|
||||
setInputUrl(node.attrs.url || '');
|
||||
setError(null);
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
@ -120,6 +140,23 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
|
||||
return (
|
||||
<NodeViewWrapper className="web-preview-block relative">
|
||||
{/* Popup Modal for Embedded Website */}
|
||||
<Modal
|
||||
isDialogOpen={popupOpen}
|
||||
onOpenChange={setPopupOpen}
|
||||
dialogTitle={previewData.title || 'Website Preview'}
|
||||
minWidth="xl"
|
||||
minHeight="xl"
|
||||
dialogContent={
|
||||
<iframe
|
||||
src={previewData.url}
|
||||
title="Embedded Website Preview"
|
||||
className="w-full h-full border-0 bg-white"
|
||||
style={{ display: 'block', borderRadius: 0 }}
|
||||
allowFullScreen
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div className={`flex w-full ${alignClass}`}> {/* CardWrapper */}
|
||||
<div className="bg-white nice-shadow rounded-xl max-w-[420px] min-w-[260px] my-2 px-6 pt-6 pb-4 relative "> {/* PreviewCard */}
|
||||
{/* Floating edit and delete buttons (only if not editing and isEditable) */}
|
||||
|
|
@ -143,71 +180,98 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Only show edit bar when editing */}
|
||||
{isEditable && editing && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 mb-2"> {/* EditBar */}
|
||||
<Globe size={18} style={{ opacity: 0.7, marginRight: 4 }} />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Enter website URL..."
|
||||
value={inputUrl}
|
||||
onChange={e => setInputUrl(e.target.value)}
|
||||
disabled={loading}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleSaveEdit(); }}
|
||||
className="flex-1 border border-gray-200 rounded-md px-2.5 py-1.5 text-sm font-sans focus:outline-none focus:border-gray-400"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveEdit}
|
||||
disabled={loading || !inputUrl}
|
||||
title="Save"
|
||||
type="button"
|
||||
className="flex items-center justify-center bg-gray-100 border-none rounded-md p-1 cursor-pointer text-gray-700 transition-colors duration-150 hover:bg-gray-200 aria-pressed:bg-blue-600 aria-pressed:text-white disabled:opacity-50"
|
||||
aria-pressed={false}
|
||||
>
|
||||
{loading ? <Save size={16} /> : <Save size={16} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
title="Cancel"
|
||||
type="button"
|
||||
className="flex items-center justify-center bg-gray-100 border-none rounded-md p-1 cursor-pointer text-gray-700 transition-colors duration-150 hover:bg-gray-200"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Button toggle and label input */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<label className="flex items-center gap-1 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showButton}
|
||||
onChange={e => {
|
||||
setShowButton(e.target.checked);
|
||||
updateAttributes({ showButton: e.target.checked });
|
||||
}}
|
||||
className="accent-blue-600"
|
||||
/>
|
||||
Show button
|
||||
</label>
|
||||
{showButton && (
|
||||
<input
|
||||
{/* Modal for editing */}
|
||||
<Modal
|
||||
isDialogOpen={modalOpen}
|
||||
onOpenChange={(open) => {
|
||||
setModalOpen(open);
|
||||
if (!open) handleCancelEdit();
|
||||
}}
|
||||
dialogTitle="Edit Web Preview Card"
|
||||
dialogDescription="Update the website preview, button, and display options."
|
||||
minWidth="md"
|
||||
dialogContent={
|
||||
<form className="space-y-6" onSubmit={e => { e.preventDefault(); handleSaveEdit(); }}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="web-url-input">Website URL</Label>
|
||||
<Input
|
||||
id="web-url-input"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={buttonLabel}
|
||||
onChange={e => {
|
||||
setButtonLabel(e.target.value);
|
||||
updateAttributes({ buttonLabel: e.target.value });
|
||||
}}
|
||||
placeholder="Button label"
|
||||
className="border border-gray-200 rounded-md px-2 py-1 text-sm font-sans focus:outline-none focus:border-gray-400"
|
||||
style={{ minWidth: 100 }}
|
||||
placeholder="Enter website URL..."
|
||||
value={inputUrl}
|
||||
onChange={e => setInputUrl(e.target.value)}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{error && <div className="text-red-600 text-xs mt-2">{error}</div>}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Button Options</Label>
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="show-button"
|
||||
checked={showButton}
|
||||
onCheckedChange={checked => setShowButton(!!checked)}
|
||||
/>
|
||||
<Label htmlFor="show-button" className="text-sm">Show button</Label>
|
||||
</div>
|
||||
{showButton && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="open-in-popup"
|
||||
checked={openInPopup}
|
||||
onCheckedChange={checked => setOpenInPopup(!!checked)}
|
||||
/>
|
||||
<Label htmlFor="open-in-popup" className="text-sm">Open in-app popup (might not work on all websites)</Label>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-col ">
|
||||
<Label htmlFor="button-label" className="text-sm">Button label</Label>
|
||||
<Input
|
||||
id="button-label"
|
||||
type="text"
|
||||
value={buttonLabel}
|
||||
onChange={e => setButtonLabel(e.target.value)}
|
||||
placeholder="Button label"
|
||||
className="w-36"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-">
|
||||
<Label>Alignment</Label>
|
||||
<div className="flex gap-2 pt-3">
|
||||
{ALIGNMENTS.map(opt => (
|
||||
<Button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
variant={alignment === opt.value ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
aria-pressed={alignment === opt.value}
|
||||
onClick={() => handleAlignmentChange(opt.value)}
|
||||
className={`rounded-full px-2 py-1 ${alignment === opt.value ? 'bg-black text-white' : ''}`}
|
||||
>
|
||||
{opt.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{error && <div className="text-red-600 text-xs mt-2">{error}</div>}
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<Button type="button" variant="outline" onClick={handleCancelEdit}>
|
||||
<span className="flex items-center"><X size={16} className="mr-1" /> Cancel</span>
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading || !inputUrl}>
|
||||
<span className="flex items-center"><Save size={16} className="mr-1" /> Save</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
{/* Only show preview card when not editing */}
|
||||
{hasPreview && !editing && (
|
||||
<>
|
||||
|
|
@ -261,39 +325,53 @@ const WebPreviewComponent: React.FC<WebPreviewProps> = ({ node, updateAttributes
|
|||
<span className="text-gray-500 text-xs truncate">{previewData.url}</span>
|
||||
</div>
|
||||
{showButton && previewData.url && (
|
||||
<a
|
||||
href={previewData.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full mt-4 rounded-xl bg-white nice-shadow text-[16px] font-semibold text-purple-600 py-2.5 px-4 text-center no-underline hover:bg-gray-50 hover:shadow-lg transition-all [&:not(:hover)]:text-black [&:hover]:text-black"
|
||||
style={{ textDecoration: 'none', color: 'black' }}
|
||||
>
|
||||
{buttonLabel || 'Visit Site'}
|
||||
</a>
|
||||
openInPopup ? (
|
||||
<button
|
||||
type="button"
|
||||
className="block w-full mt-4 rounded-xl bg-black nice-shadow text-[16px] font-semibold text-white py-2.5 px-4 text-center no-underline hover:bg-gray-900 hover:shadow-lg transition-all"
|
||||
style={{ textDecoration: 'none', color: 'white' }}
|
||||
onClick={() => setPopupOpen(true)}
|
||||
>
|
||||
{buttonLabel || 'Visit Site'}
|
||||
</button>
|
||||
) : (
|
||||
<a
|
||||
href={previewData.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full mt-4 rounded-xl bg-black nice-shadow text-[16px] font-semibold text-white py-2.5 px-4 text-center no-underline hover:bg-gray-900 hover:shadow-lg transition-all"
|
||||
style={{ textDecoration: 'none', color: 'white' }}
|
||||
>
|
||||
{buttonLabel || 'Visit Site'}
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
{/* Alignment bar in view mode */}
|
||||
{isEditable && (
|
||||
<div className="flex flex-col items-center mt-4">
|
||||
<div className="flex items-center gap-1"> {/* AlignmentBar */}
|
||||
<span className="text-xs text-gray-500 mr-1">Align:</span>
|
||||
{ALIGNMENTS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
aria-pressed={alignment === opt.value}
|
||||
onClick={() => handleAlignmentChange(opt.value)}
|
||||
title={`Align ${opt.value}`}
|
||||
type="button"
|
||||
className={`flex items-center justify-center border transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-300 p-1.5 rounded-full text-gray-600
|
||||
${alignment === opt.value
|
||||
? 'bg-gray-600 text-white border-gray-600 hover:bg-gray-700'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-100'}
|
||||
`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isEditable && !editing && (
|
||||
<div className="flex items-center gap-1 mt-2"> {/* AlignmentBar */}
|
||||
<span className="text-xs text-gray-500 mr-1">Align:</span>
|
||||
{ALIGNMENTS.map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
aria-pressed={alignment === opt.value}
|
||||
onClick={() => handleAlignmentChange(opt.value)}
|
||||
title={`Align ${opt.value}`}
|
||||
type="button"
|
||||
className={`flex items-center justify-center border transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-300 p-1.5 rounded-full text-gray-600
|
||||
${alignment === opt.value
|
||||
? 'bg-gray-600 text-white border-gray-600 hover:bg-gray-700'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-100'}
|
||||
`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue