TopBar.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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">Password</span>
  74. <Input
  75. type="password"
  76. placeholder="Optional"
  77. value={shareData["password"] as string}
  78. onInput={(e) => {
  79. shareData["password"] = (e.target as HTMLInputElement).value;
  80. }}
  81. />
  82. </div>
  83. </div>,
  84. [
  85. {
  86. text: "Confirm",
  87. onClick: async () => {
  88. const resp = await fetch("/api/share", {
  89. method: "POST",
  90. headers: { "Content-Type": "application/json" },
  91. body: JSON.stringify({
  92. id: props.id,
  93. shared: shareData["shared"],
  94. password: shareData["password"] || "",
  95. }),
  96. });
  97. const respJson = await resp.json();
  98. if (respJson.success) {
  99. shareData["submittedShared"] = shareData["shared"];
  100. shareData["submittedPassword"] = shareData["password"];
  101. globalThis.$modal?.hide();
  102. }
  103. },
  104. },
  105. ],
  106. );
  107. };
  108. const showSetting = () => {
  109. settingsData["title"] = settingsData["submittedTitle"] || props.title;
  110. globalThis.$modal?.show(
  111. "Post Settings",
  112. <div style="display: flex; align-items: center">
  113. <span style="margin-right: 8px">Title</span>
  114. <Input
  115. placeholder="Post title here"
  116. value={settingsData["title"]}
  117. onInput={(e) => {
  118. settingsData["title"] = (e.target as HTMLInputElement).value;
  119. }}
  120. />
  121. </div>,
  122. [
  123. {
  124. text: "Confirm",
  125. onClick: async () => {
  126. const resp = await fetch("/api/post", {
  127. method: "PUT",
  128. headers: { "Content-Type": "application/json" },
  129. body: JSON.stringify({
  130. id: props.id,
  131. title: settingsData["title"],
  132. }),
  133. });
  134. const respJson = await resp.json();
  135. if (respJson.success) {
  136. settingsData["submittedTitle"] = settingsData["title"];
  137. document.title = settingsData["title"];
  138. globalThis.$modal?.hide();
  139. }
  140. },
  141. },
  142. ],
  143. );
  144. };
  145. // Event listener
  146. const modeChangeListener = (e: Event) => {
  147. const { detail } = e as CustomEvent;
  148. if (
  149. detail &&
  150. (props.allowMode === detail || props.allowMode === EditorMode.Both)
  151. ) {
  152. setMode(detail);
  153. }
  154. };
  155. // Event dispatcher
  156. const modeChangeDispatcher = (mode: EditorMode) => {
  157. dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
  158. };
  159. // Init event listeners
  160. useEffect(() => {
  161. addEventListener("ModeChange", modeChangeListener);
  162. return () => {
  163. removeEventListener("ModeChange", modeChangeListener);
  164. };
  165. }, []);
  166. return (
  167. <div className="w-full flex mb-3 justify-between box-border flex-shrink-0">
  168. <div
  169. className={`border border-gray-300 dark:border-gray-700 rounded box-border text-sm ${
  170. props.allowMode !== EditorMode.Both ? "hidden" : ""
  171. }`}
  172. >
  173. <Button
  174. variant={mode === EditorMode.Edit ? "primary" : "default"}
  175. className="rounded-tr-none rounded-br-none"
  176. id="edit"
  177. type="button"
  178. onClick={() => {
  179. modeChangeDispatcher(EditorMode.Edit);
  180. }}
  181. >
  182. Edit
  183. </Button>
  184. <Button
  185. variant={mode === EditorMode.Read ? "primary" : "default"}
  186. className="rounded-none"
  187. id="read"
  188. type="button"
  189. onClick={() => {
  190. modeChangeDispatcher(EditorMode.Read);
  191. }}
  192. >
  193. Read
  194. </Button>
  195. <Button
  196. variant={mode === EditorMode.Both ? "primary" : "default"}
  197. className="rounded-tl-none rounded-bl-none"
  198. id="both"
  199. type="button"
  200. onClick={() => {
  201. modeChangeDispatcher(EditorMode.Both);
  202. }}
  203. >
  204. Both
  205. </Button>
  206. </div>
  207. {!props.isLogined
  208. ? (
  209. <div className="flex items-center justify-center w-full">
  210. <span className="leading-[30px] text-2xl font-medium text-center flex-1">
  211. {props.title}
  212. </span>
  213. <i
  214. className="bi bi-download mr-3 text-base cursor-pointer hover:text-blue-600 dark:hover:text-blue-400"
  215. onClick={download}
  216. title="Download"
  217. />
  218. <ThemeToggle />
  219. </div>
  220. )
  221. : null}
  222. {props.isLogined
  223. ? (
  224. <div className="h-[30px] leading-[28px] box-border px-2 border border-gray-300 dark:border-gray-700 rounded">
  225. <i
  226. 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"
  227. onClick={doLogout}
  228. />
  229. <i
  230. className="bi bi-house-door mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  231. onClick={goHome}
  232. />
  233. <i
  234. className="bi bi-share mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  235. onClick={showShare}
  236. />
  237. <i
  238. className="bi bi-download mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  239. onClick={download}
  240. title="Download"
  241. />
  242. <i
  243. className="bi bi-gear mr-4 text-base cursor-pointer h-4 w-4 hover:text-blue-600 dark:hover:text-blue-400"
  244. onClick={showSetting}
  245. />
  246. <ThemeToggle />
  247. </div>
  248. )
  249. : null}
  250. </div>
  251. );
  252. }