refactor: shifting lots of stuff around

This commit is contained in:
2025-07-18 15:14:30 +01:00
parent c6ad67345e
commit a89c6dc658
13 changed files with 257 additions and 255 deletions

View File

@ -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<ParentProps> = (props) => {
return (
@ -55,6 +54,7 @@ export const App = () => {
<Route path="/login" component={Login} />
<Route path="/" component={ProtectedRoute}>
<Route path="/" component={WithNotifications}>
<Route path="/" component={WithDock}>
<Route path="/" component={FrontPage} />
<Route path="/image/:imageId" component={ImagePage} />
@ -64,6 +64,7 @@ export const App = () => {
<Route path="/settings" component={Settings} />
</Route>
</Route>
</Route>
<Route path="*" component={() => <Navigate href="/" />} />
</Router>
</SearchImageContextProvider>

View File

@ -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 (
<Show when={!isAuthorized} fallback={<Navigate href="/" />}>
<form
ref={form}
onSubmit={onSubmit}
class="flex flex-col gap-2 mt-4 w-72"
>
<TextField name="email" class="flex flex-col gap-4">
<TextField.Label class="text-xl font-semibold">
Email
</TextField.Label>
<TextField.Input
pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
type="email"
class="rounded px-1 py-2"
/>
</TextField>
<Show when={submitted()}>
<TextField name="code" class="flex flex-col gap-2">
<TextField.Label class="text-xl font-semibold">
Code
</TextField.Label>
<TextField.Input class="rounded px-1 py-2" />
</TextField>
</Show>
<Button
type="submit"
class="rounded-xl bg-slate-800 px-2 py-4 text-neutral-200"
>
Submit
</Button>
</form>
</Show>
);
};

View File

@ -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<ParentProps> = (props) => {
const { onRefetchImages } = useSearchImageContext();
const notifications = Notifications(onRefetchImages);
return (
<NotificationsContext.Provider value={notifications}>
{props.children}
</NotificationsContext.Provider>
);
};
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<ParentProps> = (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 (
<Show when={isValid} fallback={<Navigate href="/login" />}>
<WithNotifications>{props.children}</WithNotifications>
</Show>
);
};

View File

@ -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 (
<svg
class={props.class}
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-amber-400"
/>
</svg>
);
case "complete":
return (
<svg
class={props.class}
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-emerald-400"
/>
</svg>
);
}
};

View File

@ -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 (
<svg
class={props.class}
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-amber-400"
/>
</svg>
);
case "complete":
return (
<svg
class={props.class}
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
fill="none"
stroke-width="3"
class="stroke-emerald-400"
/>
</svg>
);
}
};
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();

View File

@ -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<ParentProps> = (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 (
<Show when={isValid} fallback={<Navigate href="/login" />}>
{props.children}
</Show>
);
};

View File

@ -1,4 +1,3 @@
import { Separator } from "@kobalte/core/separator";
import SolidjsMarkdown from "solidjs-markdown";
import { IconNote } from "@tabler/icons-solidjs";

View File

@ -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<ReturnType<typeof Notifications>>();
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<ParentProps> = (props) => {
const { onRefetchImages } = useSearchImageContext();
const notifications = Notifications(onRefetchImages);
return (
<NotificationsContext.Provider value={notifications}>
{props.children}
</NotificationsContext.Provider>
);
};

View File

@ -0,0 +1,2 @@
export * from "./SearchImageContext";
export * from "./Notifications";

View File

@ -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"),
]),
});

View File

@ -1,3 +1,5 @@
export * from "./front";
export * from "./gallery";
export * from "./image";
export * from "./settings";
export * from "./login";

View File

@ -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 (
<Show when={!isAuthorized} fallback={<Navigate href="/" />}>
<form
ref={form}
onSubmit={onSubmit}
class="flex flex-col gap-2 mt-4 w-72"
>
<TextField name="email" class="flex flex-col gap-4">
<TextField.Label class="text-xl font-semibold">Email</TextField.Label>
<TextField.Input
pattern="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
type="email"
class="rounded px-1 py-2"
/>
</TextField>
<Show when={submitted()}>
<TextField name="code" class="flex flex-col gap-2">
<TextField.Label class="text-xl font-semibold">
Code
</TextField.Label>
<TextField.Input class="rounded px-1 py-2" />
</TextField>
</Show>
<Button
type="submit"
class="rounded-xl bg-slate-800 px-2 py-4 text-neutral-200"
>
Submit
</Button>
</form>
</Show>
);
};

View File

@ -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 = () => {