diff --git a/package-lock.json b/package-lock.json index d7049824..5581d450 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@fortawesome/react-fontawesome": "0.2.2", "@mdx-js/react": "^3.0.0", "@microsoft/clarity": "1.0.0", + "@typebot.io/react": "0.3.47", "clsx": "^2.0.0", "docusaurus-lunr-search": "^3.3.2", "docusaurus-plugin-image-zoom": "^2.0.0", @@ -4759,6 +4760,26 @@ "node": ">=10.13.0" } }, + "node_modules/@typebot.io/js": { + "version": "0.3.46", + "resolved": "https://registry.npmjs.org/@typebot.io/js/-/js-0.3.46.tgz", + "integrity": "sha512-CMTLNyQCtVnJEeod+AB4Xp7X4tKOxxtk11hnPkWbV70KoVgGEPDIu+sN4vAOUw1ZmebXIbWVGC+0F3vOAX1/lA==", + "license": "FSL-1.1-ALv2" + }, + "node_modules/@typebot.io/react": { + "version": "0.3.47", + "resolved": "https://registry.npmjs.org/@typebot.io/react/-/react-0.3.47.tgz", + "integrity": "sha512-JcOSWlpF96m3H3NWquDXz8ks8DvqB7iTGNuiWWRBPbxsQbRsrIZevO69MLCDfE8EuRDogCQhgKxCtDYuxjxceQ==", + "license": "FSL-1.1-ALv2", + "dependencies": { + "@typebot.io/js": "0.3.46", + "react": "18.2.0" + }, + "peerDependencies": { + "@typebot.io/js": "0.3.22", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", diff --git a/package.json b/package.json index 120fa650..3f784947 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-cookie-consent": "9.0.0", - "react-dom": "^18.0.0" + "react-dom": "^18.0.0", + "@typebot.io/react": "0.3.47" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.7.0", diff --git a/src/components/Support/AssistantModal.tsx b/src/components/Support/AssistantModal.tsx new file mode 100644 index 00000000..a8fd63f2 --- /dev/null +++ b/src/components/Support/AssistantModal.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { Standard } from "@typebot.io/react"; + +function AssistantModal({ isOpen, onClose }) { + if (!isOpen) return null; // Prevent modal from rendering when isOpen is false. + + // Get the current page URL. + const currentUrl = window.location.href; + + // Check if the user is on the Japanese documentation page. + const isJapanese = currentUrl.includes("/ja-jp"); + + return ( +
+
+ {/* Close the button. */} + + × + + + {/* Conditionally render the Typebot based on language. */} + +
+
+ ); +} + +const styles = { + modal: { + display: "block", + position: "fixed", + zIndex: 1000, + left: 0, + top: 0, + width: "100%", + height: "100%", + backgroundColor: "rgba(0, 0, 0, 0.7)", + }, + modalContent: { + backgroundColor: "#fff", + margin: "10% auto", + padding: "20px", + borderRadius: "10px", + width: "90%", + maxWidth: "900px", + position: "relative", // Allow absolute positioning of the close button. + }, + closeButton: { + position: "absolute", + top: "10px", + right: "20px", + fontSize: "30px", + fontWeight: "bold", + cursor: "pointer", + color: "#333", + backgroundColor: "transparent", + border: "none", + padding: "0", + zIndex: 1100, // Ensure the close button is above the modal content. + }, +}; + +export default AssistantModal; diff --git a/src/components/Support/SupportDropdownMenu.tsx b/src/components/Support/SupportDropdownMenu.tsx new file mode 100644 index 00000000..f66c35d4 --- /dev/null +++ b/src/components/Support/SupportDropdownMenu.tsx @@ -0,0 +1,165 @@ +import React, { useState, useEffect, useRef, lazy, Suspense, MouseEvent } from 'react'; +import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import { useLocation } from "@docusaurus/router"; + +// Lazy-load AssistantModal. +const AssistantModal = lazy(() => import('./AssistantModal')); + +const SupportDropdownMenu: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [storedUrl, setStoredUrl] = useState(null); + const dropdownRef = useRef(null); + const location = useLocation(); + + // Get document metadata from Docusaurus. + const { metadata } = useDoc(); + const docTitle: string = metadata?.title || "Issue with documentation page"; + + // Detect the language based on the URL path. + const isJapanese: boolean = location.pathname.startsWith("/ja-jp"); + + useEffect(() => { + if (typeof window !== "undefined") { + const currentUrl = `https://scalardb.scalar-labs.com${location.pathname}`; + localStorage.setItem("currentUrl", currentUrl); + + const savedUrl = localStorage.getItem("currentUrl"); + if (savedUrl) { + setStoredUrl(savedUrl); + } + } + }, [location]); + + const toggleDropdown = () => { + setIsOpen((prev) => !prev); + }; + + const openModal = (event: MouseEvent) => { + event.preventDefault(); + setIsModalOpen(true); + setIsOpen(false); + }; + + const closeModal = () => { + setIsModalOpen(false); + }; + + const handleSupportClick = () => { + if (typeof window !== "undefined") { + const finalUrl = storedUrl || `https://scalardb.scalar-labs.com${location.pathname}`; + const reportUrl = `https://support.scalar-labs.com/hc/ja/requests/new?ticket_form_id=8641483507983&tf_11847415366927=${encodeURIComponent(finalUrl)}`; + + window.open(reportUrl, "_blank"); + } + }; + + const githubIssueUrl: string = typeof window !== "undefined" ? (() => { + const repoUrl = "https://github.com/scalar-labs/docs-scalardb/issues/new"; + const issueTitle = encodeURIComponent( + isJapanese ? `フィードバック: \`${docTitle}\` ページ` : `Feedback: \`${docTitle}\` page` + ); + + const issueBody = encodeURIComponent( + isJapanese + ? `**ドキュメントページの URL:** ${window.location.href.replace(/#.*$/, '')} + +## 期待される動作 + +どのような動作を期待しましたか? + +## 問題の説明 + +問題の内容をわかりやすく説明してください。 + +### 再現手順 (該当する場合) + +問題を再現できる場合、手順を記載してください。 + +### スクリーンショット (該当する場合) + +該当する場合は、スクリーンショットを添付してください。 +` + : `**Documentation page URL:** ${window.location.href.replace(/#.*$/, '')} + +## Expected behavior + +What did you expect to happen? + +## Describe the problem + +Please provide a clear and concise description of what the issue is. + +### Steps to reproduce (if applicable) + +If the issue is reproducible, please list the steps to reproduce it. + +### Screenshots (if applicable) + +If applicable, add screenshots to help explain your problem. +` + ); + + const issueUrl = `${repoUrl}?title=${issueTitle}&body=${issueBody}&labels=documentation`; + + console.log("GitHub Issue URL: ", issueUrl); // Debugging line + + return issueUrl; + })() : "#"; + + useEffect(() => { + function handleClickOutside(event: MouseEvent | Event) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [isOpen]); + + return ( +
+ + +
+ +
+ {/* + {isJapanese ? "Stack Overflow をチェック" : "Check Stack Overflow"}
+ {isJapanese ? "すべてのユーザーがご利用いただけます。" : "Available to all users."} +
+
*/} + + {isJapanese ? "AI に聞く (試験運用中)" : "Ask AI (experimental)"}
+ {isJapanese ? "Scalar Membership Programにご参加の方のみご利用いただけます。" : "Available only to members of the Scalar Membership Program."} +
+
+ + {isJapanese ? "ドキュメントの問題を報告" : "Report doc issue"}
+ {isJapanese ? "このページについて何かお気づきの点がありましたら、こちらから報告いただけます。" : "If you have any feedback about this page, please submit an issue."} +
+
+ + {isModalOpen && ( + Loading...
}> + + + )} + + ); +}; + +export default SupportDropdownMenu; diff --git a/src/css/custom.css b/src/css/custom.css index 5139db01..4822ed61 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -251,3 +251,108 @@ html[data-theme="dark"] .tooltip-glossary { width: 333px !important; } } + +/* Support button and dropdown */ +.supportDropdown { + display: inline-block; + position: relative; +} + +.supportDropdownContent { + background-color: #f9f9f9; + border-radius: 8px; + box-shadow: var(--ifm-global-shadow-md); + color: var(--ifm-color-emphasis-700); + font-size: 14px; + min-width: 303px; + opacity: 0; + overflow: hidden; + padding: 8px 0px; + position: absolute; + right: 0; + transform: translateY(-10px); + transition: opacity 0.3s ease-out, transform 0.3s ease-out; + visibility: hidden; + z-index: 1; +} + +.supportDropdown:hover .supportDropdownContent { + visibility: visible; + opacity: 1; + transform: translateY(0); +} + +.supportDropdownContent { + transition-delay: 0.1s; +} + +.supportDropBtn { + align-items: center; + background-color: inherit; + border: 0; + border-radius: var(--ifm-badge-border-radius); + color: var(--ifm-navbar-link-color); + cursor: pointer; + display: flex; + font-family: var(--ifm-font-family-base); + font-size: 14.5px; + font-weight: var(--ifm-font-weight-semibold); + justify-content: space-between; + min-width: 145px; + padding: 6px 0; + text-align: left; + z-index: 1; +} + +.supportDropBtn svg { /* Keep dropdown open when moving the mouse between text and icon. */ + pointer-events: none; +} + +.supportDropdown:hover .supportDropBtn { + color: var(--ifm-color-primary); +} + +@media (max-width: 996px) { + .supportDropBtn { + font-size: 15px; + } + .supportDropdownContent { + font-size: 15px; + left: 0; + min-width: 320px; + } +} + +.supportDropdownContent a { + color: var(--ifm-dropdown-link-color); + display: block; + margin: 4px 10px; + padding: 4px 10px; + text-decoration: none; +} + +.supportDropdownContent a:hover { + background-color: var(--ifm-dropdown-hover-background-color); + border-radius: 8px; + overflow: hidden; +} + +html[data-theme="dark"] .supportDropBtn { + background-color: inherit; + border: 0; + color: #f9f9f9; +} + +html[data-theme="dark"] .supportDropdownContent { + background-color: var(--ifm-dropdown-background-color); + + a { + color: #f9f9f9; + } +} + +hr { + text-align: center; + margin: auto; + max-width: 91%; +} diff --git a/src/theme/DocItem/Layout/index.tsx b/src/theme/DocItem/Layout/index.tsx new file mode 100644 index 00000000..b191b0d6 --- /dev/null +++ b/src/theme/DocItem/Layout/index.tsx @@ -0,0 +1,95 @@ +import SupportDropdownMenu from '../../../components/Support/SupportDropdownMenu'; +import React from 'react'; +import clsx from 'clsx'; +import { useWindowSize } from '@docusaurus/theme-common'; +import { useDoc } from '@docusaurus/plugin-content-docs/client'; +import DocItemPaginator from '@theme/DocItem/Paginator'; +import DocVersionBanner from '@theme/DocVersionBanner'; +import DocVersionBadge from '@theme/DocVersionBadge'; +import DocItemFooter from '@theme/DocItem/Footer'; +import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile'; +import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop'; +import DocItemContent from '@theme/DocItem/Content'; +import DocBreadcrumbs from '@theme/DocBreadcrumbs'; +import ContentVisibility from '@theme/ContentVisibility'; + +import styles from './styles.module.css'; + +// Define the type for the useDocTOC return value. +interface DocTOC { + hidden: boolean; + mobile?: JSX.Element; + desktop?: JSX.Element; +} + +// Type for the DocItemLayout props +interface DocItemLayoutProps { + children: React.ReactNode; +} + +// Hook to handle the Table of Contents visibility and rendering +function useDocTOC(): DocTOC { + const { frontMatter, toc } = useDoc(); + const windowSize = useWindowSize(); + const hidden = frontMatter.hide_table_of_contents; + const canRender = !hidden && toc.length > 0; + + return { + hidden, + mobile: canRender ? : undefined, + desktop: canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? : undefined, + }; +} + +// DocItemLayout component +const DocItemLayout: React.FC = ({ children }) => { + const docTOC = useDocTOC(); + const { metadata, frontMatter } = useDoc(); + const hideTOC = frontMatter.hide_table_of_contents; + const windowSize = useWindowSize(); + + // Check if the current page is the home page or a version homepage. + const isHomePage = metadata.permalink === '/docs/latest/' || + /^\/docs\/\d+\.\d+\/$/.test(metadata.permalink) || + metadata.permalink === '/ja-jp/docs/latest/' || + /^\/ja-jp\/docs\/\d+\.\d+\/$/.test(metadata.permalink); + + return ( +
+
+ + +
+
+ + + {windowSize === 'mobile' && !isHomePage && ( +
+ +
+ )} + {docTOC.mobile} + {children} + +
+ +
+
+ + {!hideTOC && windowSize !== 'mobile' && ( +
+
+ {!isHomePage && ( +
+ +
+ )} + {docTOC.desktop} +
+
+ )} +
+ ); +}; + +export default DocItemLayout; diff --git a/src/theme/DocItem/Layout/styles.module.css b/src/theme/DocItem/Layout/styles.module.css new file mode 100644 index 00000000..d5aaec13 --- /dev/null +++ b/src/theme/DocItem/Layout/styles.module.css @@ -0,0 +1,10 @@ +.docItemContainer header + *, +.docItemContainer article > *:first-child { + margin-top: 0; +} + +@media (min-width: 997px) { + .docItemCol { + max-width: 75% !important; + } +}