Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions client/features/upload-form/components/UploadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ interface UploadViewProps {
fileInputRef: RefObject<HTMLInputElement | null>;
isPreviewOpen: boolean;
isPreviewLoading: boolean;
isParsingMarkdown: boolean;
isGeneratingPdf: boolean;
canGeneratePdf: boolean;
turnstileToken: string | null;
onFileSelect: (file: File) => void;
onExpirationChange: (days: number) => void;
onThemeChange: (theme: ThemeId) => void;
onPreview: () => void;
onUpload: () => void;
onGeneratePdf: () => void;
}

export function UploadView({
Expand All @@ -50,12 +54,16 @@ export function UploadView({
fileInputRef,
isPreviewOpen,
isPreviewLoading,
isParsingMarkdown,
isGeneratingPdf,
canGeneratePdf,
turnstileToken,
onFileSelect,
onExpirationChange,
onThemeChange,
onPreview,
onUpload,
onGeneratePdf,
}: UploadViewProps) {
const { t, i18n } = useTranslation();
const [isDragover, setIsDragover] = useState(false);
Expand Down Expand Up @@ -441,6 +449,22 @@ export function UploadView({
<span>{t("upload.createPage")}</span>
)}
</button>

<button
type="button"
className="w-full bg-transparent border border-border text-text-secondary py-2.5 h-10 rounded-lg text-[13px] font-medium cursor-pointer transition-all duration-200 shadow-none flex items-center justify-center gap-2 mt-2 hover:enabled:bg-surface-highlight hover:enabled:border-text-tertiary hover:enabled:text-text-primary hover:enabled:shadow-[0_2px_4px_rgba(0,0,0,0.1)] disabled:bg-surface disabled:border-border disabled:text-text-tertiary disabled:cursor-not-allowed disabled:opacity-100"
disabled={!canGeneratePdf || isParsingMarkdown || isGeneratingPdf}
onClick={onGeneratePdf}
>
{isGeneratingPdf ? (
<>
<div className="w-3.5 h-3.5 border-2 border-text-secondary/30 rounded-full border-t-text-secondary animate-spin" />
<span>{t("upload.generatingPdf")}</span>
</>
) : (
<span>{t("upload.createPdf")}</span>
)}
</button>
</div>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions client/lib/i18nResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export const resources = {
closePreview: "Close Preview",
tryLater: "Try later",
createPage: "Create Page",
createPdf: "Create PDF",
generatingPdf: "Preparing PDF...",
wikiLinkWarningTitle: "Wiki links detected",
wikiLinkWarningBody:
"External URLs inside wiki links won't be turned into pages here.",
Expand Down Expand Up @@ -282,6 +284,8 @@ export const resources = {
closePreview: "미리보기 닫기",
tryLater: "나중에 다시 시도",
createPage: "페이지 만들기",
createPdf: "PDF 생성",
generatingPdf: "PDF 준비 중...",
wikiLinkWarningTitle: "위키 링크가 감지되었어요",
wikiLinkWarningBody:
"위키 링크 안의 외부 URL은 여기서 페이지로 만들어지지 않아요.",
Expand Down Expand Up @@ -491,6 +495,8 @@ export const resources = {
closePreview: "关闭预览",
tryLater: "稍后再试",
createPage: "创建页面",
createPdf: "生成 PDF",
generatingPdf: "正在准备 PDF...",
wikiLinkWarningTitle: "检测到 Wiki 链接",
wikiLinkWarningBody: "Wiki 链接中的外部 URL 不会在这里生成页面。",
errors: {
Expand Down Expand Up @@ -693,6 +699,8 @@ export const resources = {
closePreview: "プレビューを閉じる",
tryLater: "後でもう一度試す",
createPage: "ページを作成",
createPdf: "PDF を生成",
generatingPdf: "PDF を準備中...",
wikiLinkWarningTitle: "Wiki リンクを検出しました",
wikiLinkWarningBody:
"Wiki リンク内の外部 URL はここではページ化されません。",
Expand Down
93 changes: 93 additions & 0 deletions client/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Github } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { getApiDocsHref } from "@shared/docs/api-docs";
import { ViewTemplate } from "@shared/templates/view.template";
import type { ThemeId } from "@shared/themes/theme-registry";
import { createFileRoute, Link } from "@tanstack/react-router";
import { lazy, Suspense, useEffect, useState } from "react";
Expand Down Expand Up @@ -34,6 +35,56 @@ const MarkdownParser = lazy(() =>
);

const SITE_ORIGIN = "https://mdto.page";
const PDF_IFRAME_CLEANUP_TIMEOUT_MS = 5000;
function printHtmlDocument(html: string, onPrintInitiated?: () => void): void {
const iframe = document.createElement("iframe");
iframe.setAttribute("aria-hidden", "true");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
iframe.style.opacity = "0";
iframe.style.pointerEvents = "none";
let hasNotifiedPrintInitiated = false;
const notifyPrintInitiated = () => {
if (hasNotifiedPrintInitiated) {
return;
}
hasNotifiedPrintInitiated = true;
onPrintInitiated?.();
};
const cleanupIframe = () => {
iframe.remove();
};

iframe.onerror = () => {
notifyPrintInitiated();
cleanupIframe();
};

iframe.onload = () => {
const frameWindow = iframe.contentWindow;
if (!frameWindow) {
notifyPrintInitiated();
cleanupIframe();
return;
}
const cleanupAfterPrint = () => {
frameWindow.removeEventListener("afterprint", cleanupAfterPrint);
cleanupIframe();
};
frameWindow.addEventListener("afterprint", cleanupAfterPrint);
notifyPrintInitiated();
frameWindow.focus();
frameWindow.print();
window.setTimeout(cleanupAfterPrint, PDF_IFRAME_CLEANUP_TIMEOUT_MS);
};

iframe.srcdoc = html;
document.body.appendChild(iframe);
}

export const Route = createFileRoute("/")({
head: () => ({
Expand All @@ -50,6 +101,7 @@ function Home() {
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
const [isWarningDialogOpen, setIsWarningDialogOpen] = useState(false);
const [isUploadLimitDialogOpen, setIsUploadLimitDialogOpen] = useState(false);
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
const [parsedMarkdown, setParsedMarkdown] = useState<ParsedMarkdown | null>(
null,
);
Expand Down Expand Up @@ -139,6 +191,8 @@ function Home() {

const hasWikiLink =
!isParsingMarkdown && parsedMarkdown?.metadata.hasWikiLink === true;
const canGeneratePdf =
!!selectedFile && !!parsedMarkdown && !isParsingMarkdown;

const handleUploadClick = () => {
if (!session?.user) {
Expand All @@ -148,6 +202,39 @@ function Home() {
handleUpload();
};

const handleGeneratePdf = () => {
if (
!canGeneratePdf ||
!parsedMarkdown ||
!selectedFile ||
isGeneratingPdf
) {
return;
}

setIsGeneratingPdf(true);
const expirationTime =
expirationDays === -1
? undefined
: (Date.now() + expirationDays * 24 * 60 * 60 * 1000).toString();

const printHtml = ViewTemplate({
lang: parsedMarkdown.metadata.lang,
title: selectedFile.name,
description: parsedMarkdown.metadata.description,
expiresAt: expirationTime,
html: parsedMarkdown.html,
theme: selectedTheme,
markdown: parsedMarkdown.markdown,
hasKatex: parsedMarkdown.metadata.hasKatex,
hasMermaid: parsedMarkdown.metadata.hasMermaid,
});

printHtmlDocument(printHtml.toString(), () => {
setIsGeneratingPdf(false);
});
};

return (
<>
{/* Main Container - Conditional Styles for Split View */}
Expand Down Expand Up @@ -262,8 +349,14 @@ function Home() {
onPreview={togglePreview}
isPreviewOpen={showPreview}
isPreviewLoading={isPreviewLoading}
isParsingMarkdown={isParsingMarkdown}
isGeneratingPdf={isGeneratingPdf}
canGeneratePdf={canGeneratePdf}
turnstileToken={turnstileToken}
onUpload={handleUploadClick}
onGeneratePdf={() => {
handleGeneratePdf();
}}
/>
)}
</div>
Expand Down
Loading