From a89c6dc658ab3db46c531a37d67e0f6cd5a2cba6 Mon Sep 17 00:00:00 2001 From: John Costa Date: Fri, 18 Jul 2025 15:14:30 +0100 Subject: [PATCH] refactor: shifting lots of stuff around --- frontend/src/App.tsx | 21 ++--- frontend/src/Login.tsx | 88 ------------------- frontend/src/ProtectedRoute.tsx | 68 -------------- .../notifications/LoadingCircle.tsx | 47 ++++++++++ .../notifications/ProcessingImage.tsx} | 53 +---------- .../src/components/protected-route/index.tsx | 46 ++++++++++ .../components/search-card/SearchCardNote.tsx | 33 ++++--- .../index.ts => contexts/Notifications.tsx} | 53 ++++++----- frontend/src/contexts/index.ts | 2 + frontend/src/network/notifications.ts | 11 +++ frontend/src/pages/index.ts | 2 + frontend/src/pages/login/index.tsx | 86 ++++++++++++++++++ .../settings/index.tsx} | 2 +- 13 files changed, 257 insertions(+), 255 deletions(-) delete mode 100644 frontend/src/Login.tsx delete mode 100644 frontend/src/ProtectedRoute.tsx create mode 100644 frontend/src/components/notifications/LoadingCircle.tsx rename frontend/src/{notifications/ProcessingImages.tsx => components/notifications/ProcessingImage.tsx} (65%) create mode 100644 frontend/src/components/protected-route/index.tsx rename frontend/src/{notifications/index.ts => contexts/Notifications.tsx} (71%) create mode 100644 frontend/src/contexts/index.ts create mode 100644 frontend/src/network/notifications.ts create mode 100644 frontend/src/pages/login/index.tsx rename frontend/src/{Settings.tsx => pages/settings/index.tsx} (89%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 58ca06e..87eb314 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,5 @@ import { A, Navigate, Route, Router, useNavigate } from "@solidjs/router"; import { type Component, type ParentProps } from "solid-js"; -import { Login } from "./Login"; -import { ProtectedRoute } from "./ProtectedRoute"; -import { Settings } from "./Settings"; import { IconArrowLeft, IconHome, @@ -11,8 +8,10 @@ import { } from "@tabler/icons-solidjs"; import { Entity } from "./Entity"; import { onAndroidMount } from "./mobile"; -import { FrontPage, Gallery, ImagePage } from "./pages"; +import { FrontPage, Gallery, ImagePage, Login, Settings } from "./pages"; import { SearchImageContextProvider } from "@contexts/SearchImageContext"; +import { WithNotifications } from "@contexts/Notifications"; +import { ProtectedRoute } from "@components/protected-route"; const AppWrapper: Component = (props) => { return ( @@ -55,13 +54,15 @@ export const App = () => { - - - - - + + + + + + + + - } /> diff --git a/frontend/src/Login.tsx b/frontend/src/Login.tsx deleted file mode 100644 index 87ad86d..0000000 --- a/frontend/src/Login.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Button } from "@kobalte/core/button"; -import { TextField } from "@kobalte/core/text-field"; -import { Navigate } from "@solidjs/router"; -import { type Component, Show, createSignal } from "solid-js"; -import { isTokenValid } from "./ProtectedRoute"; -import { postCode, postDemoLogin, postLogin } from "./network"; - -export const Login: Component = () => { - let form: HTMLFormElement | undefined; - - const [submitted, setSubmitted] = createSignal(false); - - const onSubmit: HTMLFormElement["onsubmit"] = async (e) => { - e.preventDefault(); - const formData = new FormData(form); - const email = formData.get("email"); - if (email == null) { - throw new Error("bruh, no email"); - } - - if (email.toString() === "demo@email.com") { - const { access, refresh } = await postDemoLogin(); - - localStorage.setItem("access", access); - localStorage.setItem("refresh", refresh); - - window.location.href = "/"; - return; - } - - if (!submitted()) { - await postLogin(email.toString()); - setSubmitted(true); - } else { - const code = formData.get("code"); - if (code == null) { - throw new Error("bruh, no code"); - } - - const { access, refresh } = await postCode( - email.toString(), - code.toString(), - ); - - localStorage.setItem("access", access); - localStorage.setItem("refresh", refresh); - - window.location.href = "/"; - } - }; - - const isAuthorized = isTokenValid(); - - return ( - }> -
- - - Email - - - - - - - Code - - - - - -
-
- ); -}; diff --git a/frontend/src/ProtectedRoute.tsx b/frontend/src/ProtectedRoute.tsx deleted file mode 100644 index d6bd913..0000000 --- a/frontend/src/ProtectedRoute.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Navigate } from "@solidjs/router"; -import { platform } from "@tauri-apps/plugin-os"; -import { jwtDecode } from "jwt-decode"; -import { type Component, type ParentProps, Show, useContext } from "solid-js"; -import { save_token } from "tauri-plugin-ios-shared-token-api"; -import { Notifications, NotificationsContext } from "./notifications"; -import { useSearchImageContext } from "./contexts/SearchImageContext"; - -export const isTokenValid = (): boolean => { - const token = localStorage.getItem("access"); - - if (token == null) { - return false; - } - - try { - jwtDecode(token); - return true; - } catch (err) { - return false; - } -}; - -const WithNotifications: Component = (props) => { - const { onRefetchImages } = useSearchImageContext(); - const notifications = Notifications(onRefetchImages); - - return ( - - {props.children} - - ); -}; - -export const useNotifications = () => { - const notifications = useContext(NotificationsContext); - if (notifications == null) { - throw new Error("Notifications must be defined when using this hook"); - } - - return notifications; -}; - -export const ProtectedRoute: Component = (props) => { - const isValid = isTokenValid(); - - if (isValid) { - const token = localStorage.getItem("access"); - if (token == null) { - throw new Error("unreachable"); - } - - if (platform() === "ios") { - // iOS share extension is a seperate process to the App. - // Therefore, we need to share our access token somewhere both the App & Share Extension can access - // This involves App Groups. - save_token(token) - .then(() => console.log("Saved token!!!")) - .catch((e) => console.error(e)); - } - } - - return ( - }> - {props.children} - - ); -}; diff --git a/frontend/src/components/notifications/LoadingCircle.tsx b/frontend/src/components/notifications/LoadingCircle.tsx new file mode 100644 index 0000000..f5d5a58 --- /dev/null +++ b/frontend/src/components/notifications/LoadingCircle.tsx @@ -0,0 +1,47 @@ +import { Component } from "solid-js"; + +export const LoadingCircle: Component<{ + status: "loading" | "complete"; + class?: string; +}> = (props) => { + switch (props.status) { + case "loading": + return ( + + + + ); + case "complete": + return ( + + + + ); + } +}; diff --git a/frontend/src/notifications/ProcessingImages.tsx b/frontend/src/components/notifications/ProcessingImage.tsx similarity index 65% rename from frontend/src/notifications/ProcessingImages.tsx rename to frontend/src/components/notifications/ProcessingImage.tsx index b7400c8..1bfa294 100644 --- a/frontend/src/notifications/ProcessingImages.tsx +++ b/frontend/src/components/notifications/ProcessingImage.tsx @@ -1,53 +1,8 @@ import { Popover } from "@kobalte/core/popover"; -import { type Component, For, Show } from "solid-js"; -import { base } from "../network"; -import { useNotifications } from "../ProtectedRoute"; - -const LoadingCircle: Component<{ - status: "loading" | "complete"; - class?: string; -}> = (props) => { - switch (props.status) { - case "loading": - return ( - - - - ); - case "complete": - return ( - - - - ); - } -}; +import { Component, For, Show } from "solid-js"; +import { LoadingCircle } from "./LoadingCircle"; +import { base } from "@network/index"; +import { useNotifications } from "@contexts/Notifications"; export const ProcessingImages: Component = () => { const notifications = useNotifications(); diff --git a/frontend/src/components/protected-route/index.tsx b/frontend/src/components/protected-route/index.tsx new file mode 100644 index 0000000..d22f722 --- /dev/null +++ b/frontend/src/components/protected-route/index.tsx @@ -0,0 +1,46 @@ +import { Navigate } from "@solidjs/router"; +import { platform } from "@tauri-apps/plugin-os"; +import { jwtDecode } from "jwt-decode"; +import { Component, ParentProps, Show } from "solid-js"; +import { save_token } from "tauri-plugin-ios-shared-token-api"; + +export const isTokenValid = (): boolean => { + const token = localStorage.getItem("access"); + + if (token == null) { + return false; + } + + try { + jwtDecode(token); + return true; + } catch (err) { + return false; + } +}; + +export const ProtectedRoute: Component = (props) => { + const isValid = isTokenValid(); + + if (isValid) { + const token = localStorage.getItem("access"); + if (token == null) { + throw new Error("unreachable"); + } + + if (platform() === "ios") { + // iOS share extension is a seperate process to the App. + // Therefore, we need to share our access token somewhere both the App & Share Extension can access + // This involves App Groups. + save_token(token) + .then(() => console.log("Saved token!!!")) + .catch((e) => console.error(e)); + } + } + + return ( + }> + {props.children} + + ); +}; diff --git a/frontend/src/components/search-card/SearchCardNote.tsx b/frontend/src/components/search-card/SearchCardNote.tsx index 976e46b..466800c 100644 --- a/frontend/src/components/search-card/SearchCardNote.tsx +++ b/frontend/src/components/search-card/SearchCardNote.tsx @@ -1,28 +1,27 @@ -import { Separator } from "@kobalte/core/separator"; import SolidjsMarkdown from "solidjs-markdown"; import { IconNote } from "@tabler/icons-solidjs"; import type { UserImage } from "../../network"; type Props = { - item: Extract; + item: Extract; }; export const SearchCardNote = ({ item }: Props) => { - const { data } = item; + const { data } = item; - return ( -
-
- -

Note

-
-

- {data.Name.length > 0 ? data.Name : "Unknown 🐞"} -

-

- {data.Content} -

-
- ); + return ( +
+
+ +

Note

+
+

+ {data.Name.length > 0 ? data.Name : "Unknown 🐞"} +

+

+ {data.Content} +

+
+ ); }; diff --git a/frontend/src/notifications/index.ts b/frontend/src/contexts/Notifications.tsx similarity index 71% rename from frontend/src/notifications/index.ts rename to frontend/src/contexts/Notifications.tsx index 0974279..cf49e47 100644 --- a/frontend/src/notifications/index.ts +++ b/frontend/src/contexts/Notifications.tsx @@ -1,27 +1,16 @@ +import { InferOutput, safeParse } from "valibot"; +import { useSearchImageContext } from "./SearchImageContext"; import { createStore } from "solid-js/store"; import { - type InferOutput, - literal, - pipe, - safeParse, - strictObject, - string, - union, - uuid, -} from "valibot"; -import { base } from "../network"; -import { createContext, createEffect, onCleanup } from "solid-js"; -import { useSearchImageContext } from "../contexts/SearchImageContext"; - -const processingImagesValidator = strictObject({ - ImageID: pipe(string(), uuid()), - ImageName: string(), - Status: union([ - literal("not-started"), - literal("in-progress"), - literal("complete"), - ]), -}); + Component, + createContext, + createEffect, + onCleanup, + ParentProps, + useContext, +} from "solid-js"; +import { base } from "@network/index"; +import { processingImagesValidator } from "@network/notifications"; type NotificationState = { ProcessingImages: Record< @@ -123,3 +112,23 @@ export const Notifications = (onCompleteImage: () => void) => { export const NotificationsContext = createContext>(); + +export const useNotifications = () => { + const notifications = useContext(NotificationsContext); + if (notifications == null) { + throw new Error("Cannot use this hook with an unmounted notifications"); + } + + return notifications; +}; + +export const WithNotifications: Component = (props) => { + const { onRefetchImages } = useSearchImageContext(); + const notifications = Notifications(onRefetchImages); + + return ( + + {props.children} + + ); +}; diff --git a/frontend/src/contexts/index.ts b/frontend/src/contexts/index.ts new file mode 100644 index 0000000..7014fdd --- /dev/null +++ b/frontend/src/contexts/index.ts @@ -0,0 +1,2 @@ +export * from "./SearchImageContext"; +export * from "./Notifications"; diff --git a/frontend/src/network/notifications.ts b/frontend/src/network/notifications.ts new file mode 100644 index 0000000..4f90128 --- /dev/null +++ b/frontend/src/network/notifications.ts @@ -0,0 +1,11 @@ +import { literal, pipe, strictObject, string, union, uuid } from "valibot"; + +export const processingImagesValidator = strictObject({ + ImageID: pipe(string(), uuid()), + ImageName: string(), + Status: union([ + literal("not-started"), + literal("in-progress"), + literal("complete"), + ]), +}); diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 55d84d3..bc80108 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,3 +1,5 @@ export * from "./front"; export * from "./gallery"; export * from "./image"; +export * from "./settings"; +export * from "./login"; diff --git a/frontend/src/pages/login/index.tsx b/frontend/src/pages/login/index.tsx new file mode 100644 index 0000000..883e9cc --- /dev/null +++ b/frontend/src/pages/login/index.tsx @@ -0,0 +1,86 @@ +import { isTokenValid } from "@components/protected-route"; +import { Button } from "@kobalte/core/button"; +import { TextField } from "@kobalte/core/text-field"; +import { postCode, postDemoLogin, postLogin } from "@network/index"; +import { Navigate } from "@solidjs/router"; +import { type Component, Show, createSignal } from "solid-js"; + +export const Login: Component = () => { + let form: HTMLFormElement | undefined; + + const [submitted, setSubmitted] = createSignal(false); + + const onSubmit: HTMLFormElement["onsubmit"] = async (e) => { + e.preventDefault(); + const formData = new FormData(form); + const email = formData.get("email"); + if (email == null) { + throw new Error("bruh, no email"); + } + + if (email.toString() === "demo@email.com") { + const { access, refresh } = await postDemoLogin(); + + localStorage.setItem("access", access); + localStorage.setItem("refresh", refresh); + + window.location.href = "/"; + return; + } + + if (!submitted()) { + await postLogin(email.toString()); + setSubmitted(true); + } else { + const code = formData.get("code"); + if (code == null) { + throw new Error("bruh, no code"); + } + + const { access, refresh } = await postCode( + email.toString(), + code.toString(), + ); + + localStorage.setItem("access", access); + localStorage.setItem("refresh", refresh); + + window.location.href = "/"; + } + }; + + const isAuthorized = isTokenValid(); + + return ( + }> +
+ + Email + + + + + + Code + + + + + +
+
+ ); +}; diff --git a/frontend/src/Settings.tsx b/frontend/src/pages/settings/index.tsx similarity index 89% rename from frontend/src/Settings.tsx rename to frontend/src/pages/settings/index.tsx index 2dc3391..40f1e9b 100644 --- a/frontend/src/Settings.tsx +++ b/frontend/src/pages/settings/index.tsx @@ -1,5 +1,5 @@ +import { Shortcuts } from "@components/shortcuts/Shortcuts"; import { Button } from "@kobalte/core/button"; -import { Shortcuts } from "./components/shortcuts/Shortcuts"; export const Settings = () => { const logout = () => {