Publish presentations: share-modal section + Preview button (UI)
Build and Deploy Verso / deploy (push) Successful in 7m53s

Wires the two entry points to the publishing backend:

- Share dialog: a "Share compiled presentation" section (owner only) with a
  public / logged-in-users-only choice, Publish/Unpublish, and a copyable link.
- Top-right toolbar: a "Preview" button that publishes a private (logged-in-
  users-only) link in one click and opens the standalone deck in a new tab
  (opened synchronously to dodge popup blockers).

Both talk to /project/:id/publish-presentation. Reuses existing i18n
(publish/unpublish/copy/preview); adds share_compiled_presentation(_info) and
presentation_link_public/private.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-01 14:39:39 +00:00
parent 18f9220e73
commit 59055aa67e
5 changed files with 202 additions and 0 deletions
@@ -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 (
<div className="ide-redesign-toolbar-button-container">
<OLButton
size="sm"
variant="secondary"
leadingIcon={<MaterialIcon type="slideshow" />}
onClick={handleClick}
disabled={loading}
>
{t('preview')}
</OLButton>
</div>
)
}
@@ -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 && (
<SubmitProjectButton cobranding={cobranding} />
)}
<PresentationPreviewButton />
<ShareProjectButton />
{getMeta('ol-showUpgradePrompt') && <UpgradeButton />}
</div>
@@ -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<PublishState>({ published: false })
const [visibility, setVisibility] = useState<Visibility>('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 (
<div className="public-access-level mb-4">
<h4>{t('share_compiled_presentation')}</h4>
<p className="small text-muted mb-2">
{t('share_compiled_presentation_info')}
</p>
<div className="mb-3">
<label className="me-3">
<input
type="radio"
name="presentation-visibility"
className="me-1"
checked={visibility === 'private'}
onChange={() => setVisibility('private')}
/>
{t('presentation_link_private')}
</label>
<label>
<input
type="radio"
name="presentation-visibility"
className="me-1"
checked={visibility === 'public'}
onChange={() => setVisibility('public')}
/>
{t('presentation_link_public')}
</label>
</div>
{state.published && state.url && (
<div className="input-group mb-2">
<input
type="text"
readOnly
className="form-control"
value={state.url}
onFocus={e => e.target.select()}
/>
<OLButton variant="secondary" onClick={copyLink}>
{copied ? t('copied') : t('copy')}
</OLButton>
</div>
)}
<div className="d-flex gap-2">
<OLButton
variant="primary"
onClick={publish}
disabled={loading || !projectId}
isLoading={loading}
>
{t('publish')}
</OLButton>
{state.published && (
<OLButton variant="secondary" onClick={unpublish} disabled={loading}>
{t('unpublish')}
</OLButton>
)}
</div>
{error && (
<div className="mt-2">
<OLNotification
type="error"
content={t('generic_something_went_wrong')}
/>
</div>
)}
</div>
)
}
@@ -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 && <PublishPresentationSection />}
{isProjectOwner ? (
<SendInvites
canAddCollaborators={canAddCollaborators}
+4
View File
@@ -1942,6 +1942,8 @@
"premium_plan_label": "Youre using <b>Overleaf Premium</b>",
"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__",