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 { A, Navigate, Route, Router, useNavigate } from "@solidjs/router";
|
||||||
import { type Component, type ParentProps } from "solid-js";
|
import { type Component, type ParentProps } from "solid-js";
|
||||||
import { Login } from "./Login";
|
|
||||||
import { ProtectedRoute } from "./ProtectedRoute";
|
|
||||||
import { Settings } from "./Settings";
|
|
||||||
import {
|
import {
|
||||||
IconArrowLeft,
|
IconArrowLeft,
|
||||||
IconHome,
|
IconHome,
|
||||||
@ -11,8 +8,10 @@ import {
|
|||||||
} from "@tabler/icons-solidjs";
|
} from "@tabler/icons-solidjs";
|
||||||
import { Entity } from "./Entity";
|
import { Entity } from "./Entity";
|
||||||
import { onAndroidMount } from "./mobile";
|
import { onAndroidMount } from "./mobile";
|
||||||
import { FrontPage, Gallery, ImagePage } from "./pages";
|
import { FrontPage, Gallery, ImagePage, Login, Settings } from "./pages";
|
||||||
import { SearchImageContextProvider } from "@contexts/SearchImageContext";
|
import { SearchImageContextProvider } from "@contexts/SearchImageContext";
|
||||||
|
import { WithNotifications } from "@contexts/Notifications";
|
||||||
|
import { ProtectedRoute } from "@components/protected-route";
|
||||||
|
|
||||||
const AppWrapper: Component<ParentProps> = (props) => {
|
const AppWrapper: Component<ParentProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
@ -55,13 +54,15 @@ export const App = () => {
|
|||||||
<Route path="/login" component={Login} />
|
<Route path="/login" component={Login} />
|
||||||
|
|
||||||
<Route path="/" component={ProtectedRoute}>
|
<Route path="/" component={ProtectedRoute}>
|
||||||
<Route path="/" component={WithDock}>
|
<Route path="/" component={WithNotifications}>
|
||||||
<Route path="/" component={FrontPage} />
|
<Route path="/" component={WithDock}>
|
||||||
<Route path="/image/:imageId" component={ImagePage} />
|
<Route path="/" component={FrontPage} />
|
||||||
<Route path="/entity/:entityId" component={Entity} />
|
<Route path="/image/:imageId" component={ImagePage} />
|
||||||
<Route path="/gallery/:entity" component={Gallery} />
|
<Route path="/entity/:entityId" component={Entity} />
|
||||||
|
<Route path="/gallery/:entity" component={Gallery} />
|
||||||
|
</Route>
|
||||||
|
<Route path="/settings" component={Settings} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings" component={Settings} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" component={() => <Navigate href="/" />} />
|
<Route path="*" component={() => <Navigate href="/" />} />
|
||||||
|
@ -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 { Popover } from "@kobalte/core/popover";
|
||||||
import { type Component, For, Show } from "solid-js";
|
import { Component, For, Show } from "solid-js";
|
||||||
import { base } from "../network";
|
import { LoadingCircle } from "./LoadingCircle";
|
||||||
import { useNotifications } from "../ProtectedRoute";
|
import { base } from "@network/index";
|
||||||
|
import { useNotifications } from "@contexts/Notifications";
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProcessingImages: Component = () => {
|
export const ProcessingImages: Component = () => {
|
||||||
const notifications = useNotifications();
|
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,28 +1,27 @@
|
|||||||
import { Separator } from "@kobalte/core/separator";
|
|
||||||
import SolidjsMarkdown from "solidjs-markdown";
|
import SolidjsMarkdown from "solidjs-markdown";
|
||||||
|
|
||||||
import { IconNote } from "@tabler/icons-solidjs";
|
import { IconNote } from "@tabler/icons-solidjs";
|
||||||
import type { UserImage } from "../../network";
|
import type { UserImage } from "../../network";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
item: Extract<UserImage, { type: "note" }>;
|
item: Extract<UserImage, { type: "note" }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SearchCardNote = ({ item }: Props) => {
|
export const SearchCardNote = ({ item }: Props) => {
|
||||||
const { data } = item;
|
const { data } = item;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="h-full inset-0 p-3 bg-green-50">
|
<div class="h-full inset-0 p-3 bg-green-50">
|
||||||
<div class="flex mb-1 items-center gap-1">
|
<div class="flex mb-1 items-center gap-1">
|
||||||
<IconNote size={14} class="text-neutral-500" />
|
<IconNote size={14} class="text-neutral-500" />
|
||||||
<p class="text-xs text-neutral-500">Note</p>
|
<p class="text-xs text-neutral-500">Note</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-neutral-900 font-bold mb-1">
|
<p class="text-sm text-neutral-900 font-bold mb-1">
|
||||||
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
|
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-neutral-700">
|
<p class="text-xs text-neutral-700">
|
||||||
<SolidjsMarkdown>{data.Content}</SolidjsMarkdown>
|
<SolidjsMarkdown>{data.Content}</SolidjsMarkdown>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,27 +1,16 @@
|
|||||||
|
import { InferOutput, safeParse } from "valibot";
|
||||||
|
import { useSearchImageContext } from "./SearchImageContext";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import {
|
import {
|
||||||
type InferOutput,
|
Component,
|
||||||
literal,
|
createContext,
|
||||||
pipe,
|
createEffect,
|
||||||
safeParse,
|
onCleanup,
|
||||||
strictObject,
|
ParentProps,
|
||||||
string,
|
useContext,
|
||||||
union,
|
} from "solid-js";
|
||||||
uuid,
|
import { base } from "@network/index";
|
||||||
} from "valibot";
|
import { processingImagesValidator } from "@network/notifications";
|
||||||
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"),
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
|
|
||||||
type NotificationState = {
|
type NotificationState = {
|
||||||
ProcessingImages: Record<
|
ProcessingImages: Record<
|
||||||
@ -123,3 +112,23 @@ export const Notifications = (onCompleteImage: () => void) => {
|
|||||||
|
|
||||||
export const NotificationsContext =
|
export const NotificationsContext =
|
||||||
createContext<ReturnType<typeof Notifications>>();
|
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 "./front";
|
||||||
export * from "./gallery";
|
export * from "./gallery";
|
||||||
export * from "./image";
|
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 { Button } from "@kobalte/core/button";
|
||||||
import { Shortcuts } from "./components/shortcuts/Shortcuts";
|
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const logout = () => {
|
const logout = () => {
|
Reference in New Issue
Block a user