Publish presentations: share-modal section + Preview button (UI)
Build and Deploy Verso / deploy (push) Successful in 7m53s
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:
+52
@@ -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>
|
||||
|
||||
+141
@@ -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}
|
||||
|
||||
@@ -1942,6 +1942,8 @@
|
||||
"premium_plan_label": "You’re 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__",
|
||||
|
||||
Reference in New Issue
Block a user