TopBar.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import { useEffect, useState } from "preact/hooks";
  2. import { EditorMode } from "./Editor.tsx";
  3. import Button from "../components/form/Button.tsx";
  4. import Input from "../components/form/Input.tsx";
  5. import Checkbox from "../components/form/Checkbox.tsx";
  6. import ThemeToggle from "./ThemeToggle.tsx";
  7. interface TopBarProps {
  8. allowMode: EditorMode;
  9. isLogined: boolean;
  10. shared: boolean;
  11. title: string;
  12. id: string;
  13. }
  14. const settingsData: { [key: string]: string } = {};
  15. const shareData: { [key: string]: boolean | string } = {};
  16. export default function TopBar(props: TopBarProps) {
  17. const [mode, setMode] = useState(props.allowMode);
  18. const doLogout = async () => {
  19. const resp = await fetch("/api/user/logout");
  20. const respJson = await resp.json();
  21. if (respJson.success) {
  22. location.href = "/login";
  23. return true;
  24. }
  25. return false;
  26. };
  27. const goHome = () => {
  28. location.href = "/";
  29. };
  30. const download = () => {
  31. dispatchEvent(new CustomEvent("DownloadRequest", { detail: props.title }));
  32. };
  33. const showShare = () => {
  34. shareData["shared"] = shareData["submittedShared"] !== undefined
  35. ? shareData["submittedShared"] as boolean
  36. : props.shared;
  37. shareData["password"] = shareData["submittedPassword"] || "";
  38. globalThis.$modal?.show(
  39. "Share options",
  40. <div>
  41. <div style="display: flex; align-items: center">
  42. <Checkbox
  43. label="Share to friends"
  44. checked={shareData["shared"] as boolean}
  45. onChange={(e) => {
  46. shareData["shared"] = (e.target as HTMLInputElement).checked;
  47. const extraEl = document.querySelector(
  48. "#share-extra",
  49. ) as HTMLDivElement;
  50. extraEl.style.display = shareData["shared"] ? "flex" : "none";
  51. }}
  52. />
  53. <Button
  54. type="button"
  55. id="shared-button"
  56. className="ml-2"
  57. style={`${!shareData["shared"] ? ";visibility: hidden" : ""}`}
  58. onClick={async () => {
  59. await navigator.clipboard.writeText(
  60. location.href.split("?")[0],
  61. );
  62. }}
  63. >
  64. Copy Link
  65. </Button>
  66. </div>
  67. <div
  68. id="share-extra"
  69. style={`display: ${
  70. shareData["shared"] ? "flex" : "none"
  71. }; align-items: center; margin-top: 12px`}
  72. >
  73. <span style="margin-right: 8px; white-space: nowrap">
  74. Password (optional)
  75. </span>
  76. <Input
  77. type="password"
  78. placeholder="Password"
  79. value={shareData["password"] as string}
  80. onInput={(e) => {
  81. shareData["password"] = (e.target as HTMLInputElement).value;
  82. }}
  83. />
  84. </div>
  85. </div>,
  86. [
  87. {
  88. text: "Confirm",
  89. onClick: async () => {
  90. const resp = await fetch("/api/share", {
  91. method: "POST",
  92. headers: { "Content-Type": "application/json" },
  93. body: JSON.stringify({
  94. id: props.id,
  95. shared: shareData["shared"],
  96. password: shareData["password"] || "",
  97. }),
  98. });
  99. const respJson = await resp.json();
  100. if (respJson.success) {
  101. shareData["submittedShared"] = shareData["shared"];
  102. shareData["submittedPassword"] = shareData["password"];
  103. globalThis.$modal?.hide();
  104. }
  105. },
  106. },
  107. ],
  108. );
  109. };
  110. const showSetting = () => {
  111. settingsData["title"] = settingsData["submittedTitle"] || props.title;
  112. globalThis.$modal?.show(
  113. "Post Settings",
  114. <div style="display: flex; align-items: center">
  115. <span style="margin-right: 8px">Title</span>
  116. <Input
  117. placeholder="Post title here"
  118. value={settingsData["title"]}
  119. onInput={(e) => {
  120. settingsData["title"] = (e.target as HTMLInputElement).value;
  121. }}
  122. />
  123. </div>,
  124. [
  125. {
  126. text: "Confirm",
  127. onClick: async () => {
  128. const resp = await fetch("/api/post", {
  129. method: "PUT",
  130. headers: { "Content-Type": "application/json" },
  131. body: JSON.stringify({
  132. id: props.id,
  133. title: settingsData["title"],
  134. }),
  135. });
  136. const respJson = await resp.json();
  137. if (respJson.success) {
  138. settingsData["submittedTitle"] = settingsData["title"];
  139. document.title = settingsData["title"];
  140. globalThis.$modal?.hide();
  141. }
  142. },
  143. },
  144. ],
  145. );
  146. };
  147. // Event listener
  148. const modeChangeListener = (e: Event) => {
  149. const { detail } = e as CustomEvent;
  150. if (
  151. detail &&
  152. (props.allowMode === detail || props.allowMode === EditorMode.Both)
  153. ) {
  154. setMode(detail);
  155. }
  156. };
  157. // Event dispatcher
  158. const modeChangeDispatcher = (mode: EditorMode) => {
  159. dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
  160. };
  161. // Init event listeners
  162. useEffect(() => {
  163. addEventListener("ModeChange", modeChangeListener);
  164. return () => {
  165. removeEventListener("ModeChange", modeChangeListener);
  166. };
  167. }, []);
  168. return (
  169. <div className="w-full flex mb-3 justify-between box-border flex-shrink-0">
  170. <div
  171. className={`border border-gray-300 dark:border-gray-700 rounded box-border text-sm ${
  172. props.allowMode !== EditorMode.Both ? "hidden" : ""
  173. }`}
  174. >
  175. <Button
  176. variant={mode === EditorMode.Edit ? "primary" : "default"}
  177. className="rounded-tr-none rounded-br-none"
  178. id="edit"
  179. type="button"
  180. onClick={() => {
  181. modeChangeDispatcher(EditorMode.Edit);
  182. }}
  183. >
  184. Edit
  185. </Button>
  186. <Button
  187. variant={mode === EditorMode.Read ? "primary" : "default"}
  188. className="rounded-none"
  189. id="read"
  190. type="button"
  191. onClick={() => {
  192. modeChangeDispatcher(EditorMode.Read);
  193. }}
  194. >
  195. Read
  196. </Button>
  197. <Button
  198. variant={mode === EditorMode.Both ? "primary" : "default"}
  199. className="rounded-tl-none rounded-bl-none"
  200. id="both"
  201. type="button"
  202. onClick={() => {
  203. modeChangeDispatcher(EditorMode.Both);
  204. }}
  205. >
  206. Both
  207. </Button>
  208. </div>
  209. {!props.isLogined
  210. ? (
  211. <div className="flex items-center justify-center w-full">
  212. <span className="leading-[30px] text-2xl font-medium text-center flex-1">
  213. {props.title}
  214. </span>
  215. <i
  216. className="bi bi-download mr-3 text-base cursor-pointer hover:text-blue-600 dark:hover:text-blue-400"
  217. onClick={download}
  218. title="Download"
  219. />
  220. <ThemeToggle />
  221. </div>
  222. )
  223. : null}
  224. {props.isLogined
  225. ? (
  226. <div className="h-[30px] leading-[28px] box-border px-2 border border-gray-300 dark:border-gray-700 rounded">
  227. <i
  228. className="bi bi-box-arrow-left mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  229. onClick={doLogout}
  230. />
  231. <i
  232. className="bi bi-house-door mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  233. onClick={goHome}
  234. />
  235. <i
  236. className="bi bi-share mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  237. onClick={showShare}
  238. />
  239. <i
  240. className="bi bi-download mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  241. onClick={download}
  242. title="Download"
  243. />
  244. <i
  245. className="bi bi-gear mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  246. onClick={showSetting}
  247. />
  248. <ThemeToggle />
  249. </div>
  250. )
  251. : null}
  252. </div>
  253. );
  254. }