TopBar.tsx 7.6 KB

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