refactor: shifting lots of stuff around
This commit is contained in:
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
47
frontend/src/components/notifications/LoadingCircle.tsx
Normal file
47
frontend/src/components/notifications/LoadingCircle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
};
|
@ -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();
|
46
frontend/src/components/protected-route/index.tsx
Normal file
46
frontend/src/components/protected-route/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import { Separator } from "@kobalte/core/separator";
|
||||
import SolidjsMarkdown from "solidjs-markdown";
|
||||
|
||||
import { IconNote } from "@tabler/icons-solidjs";
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
2
frontend/src/contexts/index.ts
Normal file
2
frontend/src/contexts/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./SearchImageContext";
|
||||
export * from "./Notifications";
|
11
frontend/src/network/notifications.ts
Normal file
11
frontend/src/network/notifications.ts
Normal 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"),
|
||||
]),
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
export * from "./front";
|
||||
export * from "./gallery";
|
||||
export * from "./image";
|
||||
export * from "./settings";
|
||||
export * from "./login";
|
||||
|
86
frontend/src/pages/login/index.tsx
Normal file
86
frontend/src/pages/login/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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 = () => {
|
Reference in New Issue
Block a user