diff --git a/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx b/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx
index c6f13e8e2c..386dfbcb4e 100644
--- a/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx
+++ b/services/web/frontend/js/features/project-list/components/dropdown/actions-dropdown.tsx
@@ -15,6 +15,7 @@ import LeaveProjectButton from '../table/cells/action-buttons/leave-project-butt
import DeleteProjectButton from '../table/cells/action-buttons/delete-project-button'
import { Project } from '../../../../../../types/project/dashboard/api'
import CompileAndDownloadProjectPDFButton from '../table/cells/action-buttons/compile-and-download-project-pdf-button'
+import DownloadPresentationButton from '../table/cells/action-buttons/download-presentation-button'
import RenameProjectButton from '../table/cells/action-buttons/rename-project-button'
import MaterialIcon from '@/shared/components/material-icon'
import OLSpinner from '@/shared/components/ol/ol-spinner'
@@ -77,32 +78,82 @@ function ActionsDropdown({ project }: ActionDropdownProps) {
)}
-
- {(text, pendingCompile, downloadProject) => (
-
- {
- e.stopPropagation()
- downloadProject()
- }}
- leadingIcon={
- pendingCompile ? (
-
- ) : (
- 'picture_as_pdf'
- )
- }
- >
- {text}
-
-
- )}
-
+ {project.compiler === 'quarto' &&
+ project.quartoFlavor === 'revealjs' ? (
+
+ {(startExport, exporting) => (
+ <>
+
+ startExport('html')}
+ disabled={exporting !== null}
+ leadingIcon={
+ exporting === 'html' ? (
+
+ ) : (
+ 'download'
+ )
+ }
+ >
+ {t('download_as_standalone_html')}
+
+
+
+ startExport('pdf')}
+ disabled={exporting !== null}
+ leadingIcon={
+ exporting === 'pdf' ? (
+
+ ) : (
+ 'picture_as_pdf'
+ )
+ }
+ >
+ {t('download_as_pdf_slides')}
+
+
+ >
+ )}
+
+ ) : (
+
+ {(text, pendingCompile, downloadProject) => (
+
+ {
+ e.stopPropagation()
+ downloadProject()
+ }}
+ leadingIcon={
+ pendingCompile ? (
+
+ ) : (
+ 'picture_as_pdf'
+ )
+ }
+ >
+ {text}
+
+
+ )}
+
+ )}
{(text, handleOpenModal) => (
diff --git a/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-presentation-button.tsx b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-presentation-button.tsx
new file mode 100644
index 0000000000..68ff87d241
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/table/cells/action-buttons/download-presentation-button.tsx
@@ -0,0 +1,190 @@
+import { memo, useCallback, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Project } from '../../../../../../../../types/project/dashboard/api'
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownToggle,
+} from '@/shared/components/dropdown/dropdown-menu'
+import {
+ OLModal,
+ OLModalBody,
+ OLModalFooter,
+ OLModalHeader,
+ OLModalTitle,
+} from '@/shared/components/ol/ol-modal'
+import OLButton from '@/shared/components/ol/ol-button'
+import OLSpinner from '@/shared/components/ol/ol-spinner'
+import LoadingSpinner from '@/shared/components/loading-spinner'
+import MaterialIcon from '@/shared/components/material-icon'
+
+type ExportFormat = 'html' | 'pdf'
+
+type DownloadPresentationButtonProps = {
+ project: Project
+ children: (
+ startExport: (format: ExportFormat) => void,
+ exporting: ExportFormat | null
+ ) => React.ReactElement
+}
+
+function DownloadPresentationButton({
+ project,
+ children,
+}: DownloadPresentationButtonProps) {
+ const { t } = useTranslation()
+ const [exporting, setExporting] = useState(null)
+ const [exportError, setExportError] = useState(null)
+ const requestIdRef = useRef(0)
+
+ const dismiss = useCallback(() => {
+ requestIdRef.current += 1
+ setExporting(null)
+ setExportError(null)
+ }, [])
+
+ const startExport = useCallback(
+ async (format: ExportFormat) => {
+ const requestId = ++requestIdRef.current
+ setExportError(null)
+ setExporting(format)
+ try {
+ const response = await fetch(
+ `/project/${project.id}/presentation-export/${format}`,
+ { credentials: 'same-origin' }
+ )
+ if (requestId !== requestIdRef.current) return
+ if (!response.ok) {
+ const text = await response.text()
+ if (requestId !== requestIdRef.current) return
+ setExporting(null)
+ setExportError(text || `Export failed (HTTP ${response.status})`)
+ return
+ }
+ const blob = await response.blob()
+ if (requestId !== requestIdRef.current) return
+ const disposition = response.headers.get('Content-Disposition')
+ const match = disposition?.match(/filename="?([^"]+)"?/)
+ const filename =
+ match ? match[1] : `presentation.${format === 'pdf' ? 'pdf' : 'html'}`
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ URL.revokeObjectURL(url)
+ setExporting(null)
+ } catch (err) {
+ if (requestId !== requestIdRef.current) return
+ setExporting(null)
+ setExportError(err instanceof Error ? err.message : String(err))
+ }
+ },
+ [project.id]
+ )
+
+ return (
+ <>
+ {children(startExport, exporting)}
+
+
+
+ {exportError
+ ? t('presentation_export_failed')
+ : t('preparing_your_download')}
+
+
+
+ {exporting !== null ? (
+
+ ) : (
+
+ {exportError}
+
+ )}
+
+
+
+ {t('close')}
+
+
+
+ >
+ )
+}
+
+// For ActionsCell (desktop icon buttons row): an icon button that opens a
+// dropdown with PDF and HTML export choices.
+const DownloadPresentationButtonTooltip = memo(
+ function DownloadPresentationButtonTooltip({
+ project,
+ }: { project: Project }) {
+ const { t } = useTranslation()
+
+ if (project.compiler !== 'quarto' || project.quartoFlavor !== 'revealjs') {
+ return null
+ }
+
+ return (
+
+ {(startExport, exporting) => (
+
+
+ {exporting !== null ? (
+
+ ) : (
+
+ )}
+
+
+
+ startExport('html')}
+ disabled={exporting !== null}
+ leadingIcon="download"
+ >
+ {t('download_as_standalone_html')}
+
+
+
+ startExport('pdf')}
+ disabled={exporting !== null}
+ leadingIcon="picture_as_pdf"
+ >
+ {t('download_as_pdf_slides')}
+
+
+
+
+ )}
+
+ )
+ }
+)
+
+export default memo(DownloadPresentationButton)
+export { DownloadPresentationButtonTooltip }
diff --git a/services/web/frontend/js/features/project-list/components/table/cells/actions-cell.tsx b/services/web/frontend/js/features/project-list/components/table/cells/actions-cell.tsx
index 2e0c3f336f..1934684c04 100644
--- a/services/web/frontend/js/features/project-list/components/table/cells/actions-cell.tsx
+++ b/services/web/frontend/js/features/project-list/components/table/cells/actions-cell.tsx
@@ -8,17 +8,25 @@ import { DownloadProjectButtonTooltip } from './action-buttons/download-project-
import { LeaveProjectButtonTooltip } from './action-buttons/leave-project-button'
import { DeleteProjectButtonTooltip } from './action-buttons/delete-project-button'
import { CompileAndDownloadProjectPDFButtonTooltip } from './action-buttons/compile-and-download-project-pdf-button'
+import { DownloadPresentationButtonTooltip } from './action-buttons/download-presentation-button'
type ActionsCellProps = {
project: Project
}
+const isQuartoSlides = (project: Project) =>
+ project.compiler === 'quarto' && project.quartoFlavor === 'revealjs'
+
export default function ActionsCell({ project }: ActionsCellProps) {
return (
<>
-
+ {isQuartoSlides(project) ? (
+
+ ) : (
+
+ )}