TopBar.tsx 6.7 KB

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