diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/presentation-preview-button.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/presentation-preview-button.tsx new file mode 100644 index 0000000000..d6f4cf3ce5 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/toolbar/presentation-preview-button.tsx @@ -0,0 +1,52 @@ +import OLButton from '@/shared/components/ol/ol-button' +import MaterialIcon from '@/shared/components/material-icon' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { postJSON } from '@/infrastructure/fetch-json' +import getMeta from '@/utils/meta' + +// One-click shortcut: publish the compiled presentation as a private +// (logged-in-users-only) standalone page and open it in a new tab. For finer +// control (public links, unpublish) use the Share dialog. +export default function PresentationPreviewButton() { + const { t } = useTranslation() + const [loading, setLoading] = useState(false) + const projectId = getMeta('ol-project_id') + + const handleClick = useCallback(() => { + setLoading(true) + // Open the tab synchronously inside the click handler to avoid popup + // blockers, then redirect it once we have the published URL. + const win = window.open('', '_blank') + postJSON(`/project/${projectId}/publish-presentation`, { + body: { visibility: 'private' }, + }) + .then((data: { url?: string }) => { + if (data?.url && win) { + win.location.href = data.url + } else if (data?.url) { + window.open(data.url, '_blank') + } else if (win) { + win.close() + } + }) + .catch(() => { + if (win) win.close() + }) + .finally(() => setLoading(false)) + }, [projectId]) + + return ( +
+ } + onClick={handleClick} + disabled={loading} + > + {t('preview')} + +
+ ) +} diff --git a/services/web/frontend/js/features/ide-react/components/toolbar/toolbar.tsx b/services/web/frontend/js/features/ide-react/components/toolbar/toolbar.tsx index ee44004bcd..8e1b687c98 100644 --- a/services/web/frontend/js/features/ide-react/components/toolbar/toolbar.tsx +++ b/services/web/frontend/js/features/ide-react/components/toolbar/toolbar.tsx @@ -3,6 +3,7 @@ import { ToolbarMenuBar } from './menu-bar' import { ToolbarProjectTitle } from './project-title' import { OnlineUsers } from './online-users' import ShareProjectButton from './share-project-button' +import PresentationPreviewButton from './presentation-preview-button' import ChangeLayoutButton from './change-layout-button' import ShowHistoryButton from './show-history-button' import { useLayoutContext } from '@/shared/context/layout-context' @@ -59,6 +60,7 @@ export const Toolbar = () => { {shouldDisplaySubmitButton && cobranding && ( )} + {getMeta('ol-showUpgradePrompt') && } diff --git a/services/web/frontend/js/features/share-project-modal/components/publish-presentation-section.tsx b/services/web/frontend/js/features/share-project-modal/components/publish-presentation-section.tsx new file mode 100644 index 0000000000..01e0ed43e0 --- /dev/null +++ b/services/web/frontend/js/features/share-project-modal/components/publish-presentation-section.tsx @@ -0,0 +1,141 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useProjectContext } from '@/shared/context/project-context' +import { getJSON, postJSON, deleteJSON } from '@/infrastructure/fetch-json' +import getMeta from '@/utils/meta' +import OLButton from '@/shared/components/ol/ol-button' +import OLNotification from '@/shared/components/ol/ol-notification' + +type Visibility = 'public' | 'private' + +type PublishState = { + published: boolean + url?: string + visibility?: Visibility + publishedAt?: string +} + +// "Share only the compiled result": publish the compiled HTML/RevealJS deck as +// a standalone, linkable page (public or logged-in-users-only), independent of +// the editor. Re-publishing keeps the same link. +export default function PublishPresentationSection() { + const { t } = useTranslation() + const { project } = useProjectContext() + const projectId = project?._id || getMeta('ol-project_id') + + const [state, setState] = useState({ published: false }) + const [visibility, setVisibility] = useState('private') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(false) + const [copied, setCopied] = useState(false) + + useEffect(() => { + if (!projectId) return + getJSON(`/project/${projectId}/publish-presentation`) + .then((data: PublishState) => { + setState(data) + if (data.visibility) setVisibility(data.visibility) + }) + .catch(() => {}) + }, [projectId]) + + const publish = useCallback(() => { + setLoading(true) + setError(false) + postJSON(`/project/${projectId}/publish-presentation`, { + body: { visibility }, + }) + .then((data: PublishState) => setState(data)) + .catch(() => setError(true)) + .finally(() => setLoading(false)) + }, [projectId, visibility]) + + const unpublish = useCallback(() => { + setLoading(true) + setError(false) + deleteJSON(`/project/${projectId}/publish-presentation`) + .then(() => setState({ published: false })) + .catch(() => setError(true)) + .finally(() => setLoading(false)) + }, [projectId]) + + const copyLink = useCallback(() => { + if (state.url && navigator.clipboard) { + navigator.clipboard.writeText(state.url) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + }, [state.url]) + + return ( +
+

{t('share_compiled_presentation')}

+

+ {t('share_compiled_presentation_info')} +

+ +
+ + +
+ + {state.published && state.url && ( +
+ e.target.select()} + /> + + {copied ? t('copied') : t('copy')} + +
+ )} + +
+ + {t('publish')} + + {state.published && ( + + {t('unpublish')} + + )} +
+ + {error && ( +
+ +
+ )} +
+ ) +} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx index bccf27b191..51b9b0cce7 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.tsx @@ -15,6 +15,7 @@ import OLNotification from '@/shared/components/ol/ol-notification' import ErrorMessage from '@/features/share-project-modal/components/error-message' import ProjectAccess from '@/features/share-project-modal/components/project-access' import InvitedPeople from '@/features/share-project-modal/components/invited-people' +import PublishPresentationSection from '@/features/share-project-modal/components/publish-presentation-section' type ShareModalBodyProps = { isInvitedPeopleScreen: boolean @@ -114,6 +115,8 @@ export default function ShareModalBody({ return ( <> + {isProjectOwner && } + {isProjectOwner ? ( Overleaf Premium", "preparing_for_export": "Preparing for export…", "presentation": "Presentation", + "presentation_link_private": "Only logged-in users", + "presentation_link_public": "Anyone with the link", "presentation_mode": "Presentation mode", "press_shift_space_for_suggestions": "Press Shift+Space for suggestions", "press_space_to_open_the_ai_assistant": "Press Space to open the AI assistant", @@ -2348,6 +2350,8 @@ "settings": "Settings", "setup_another_account_under_a_personal_email_address": "Set up another Overleaf account under a personal email address.", "share": "Share", + "share_compiled_presentation": "Share compiled presentation", + "share_compiled_presentation_info": "Publish the compiled presentation as a standalone page you can link to, outside the editor. Re-publish to update it.", "share_feedback": "Share feedback", "share_project": "Share Project", "share_project_name": "Share __projectName__",