feat(ui): basic dark mode, better day overview chart
This commit is contained in:
parent
91a670c72a
commit
bba7c3554d
8 changed files with 405 additions and 106 deletions
104
src/App.tsx
104
src/App.tsx
|
@ -1,18 +1,104 @@
|
||||||
import { Route } from "@solidjs/router";
|
import { A, Route, Router, useNavigate } from "@solidjs/router";
|
||||||
import { type Component } from "solid-js";
|
import { createEffect, Show, Suspense, type Component } from "solid-js";
|
||||||
import { DmId, GroupId, Home, Overview, preloadDmId, Privacy } from "./pages";
|
import { DmId, GroupId, Home, Overview, preloadDmId, Privacy } from "./pages";
|
||||||
|
|
||||||
import "./app.css";
|
import "./app.css";
|
||||||
|
import { MetaProvider } from "@solidjs/meta";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
|
import { Callout, CalloutTitle, CalloutContent } from "./components/ui/callout";
|
||||||
|
import { dbLoaded } from "./db";
|
||||||
|
import { hasCashedData } from "./lib/db-cache";
|
||||||
|
import { isWasmSupported } from "./lib/utils";
|
||||||
|
import { ColorModeProvider, ColorModeScript, createLocalStorageManager } from "@kobalte/core";
|
||||||
|
import { ModeToggle } from "./components/ui/mode-toggle";
|
||||||
|
|
||||||
|
const NO_DATA_NEEDED_PAGES = ["/", "/privacy"];
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
|
const storageManager = createLocalStorageManager("vite-ui-theme");
|
||||||
return (
|
return (
|
||||||
<>
|
<div class="mx-auto max-w-(--breakpoint-2xl)">
|
||||||
<Route path="/" component={Home} />
|
<MetaProvider>
|
||||||
<Route path="/overview" component={Overview} />
|
<Router
|
||||||
<Route path="/dm/:dmid" component={DmId} preload={preloadDmId} />
|
root={(props) => {
|
||||||
<Route path="/group/:groupid" component={GroupId} />
|
const navigate = useNavigate();
|
||||||
<Route path="/privacy" component={Privacy} />
|
|
||||||
</>
|
createEffect(() => {
|
||||||
|
if (!dbLoaded() && !hasCashedData() && !NO_DATA_NEEDED_PAGES.includes(props.location.pathname)) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const wasmSupport = isWasmSupported();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ColorModeScript storageType={storageManager.type} />
|
||||||
|
<ColorModeProvider storageManager={storageManager}>
|
||||||
|
<header class="flex justify-end bg-accent p-4">
|
||||||
|
<ModeToggle />
|
||||||
|
</header>
|
||||||
|
<Show when={!wasmSupport}>
|
||||||
|
<Portal>
|
||||||
|
<div class="fixed inset-0 mx-4 flex flex-col items-center justify-center backdrop-blur-lg">
|
||||||
|
<Callout variant="error">
|
||||||
|
Your browser does not support WebAssembly, which is required for this site to work with the
|
||||||
|
big amount of data a signal backup contains.
|
||||||
|
<br />
|
||||||
|
Please try a different browser.
|
||||||
|
</Callout>
|
||||||
|
</div>
|
||||||
|
</Portal>
|
||||||
|
</Show>
|
||||||
|
<Show
|
||||||
|
when={props.location.pathname !== "/" && !dbLoaded() && hasCashedData()}
|
||||||
|
fallback={
|
||||||
|
<Show when={!dbLoaded() && hasCashedData()}>
|
||||||
|
<Callout variant="default" class="m-4">
|
||||||
|
There is currently no backup database loaded, but you can watch statistics that have been
|
||||||
|
cached, meaning only chats you already opened or chats that were preloaded.
|
||||||
|
<br />
|
||||||
|
<A
|
||||||
|
href="/overview"
|
||||||
|
onClick={() => {
|
||||||
|
umami.track("Watch cached statistics");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Watch cached statistics
|
||||||
|
</A>
|
||||||
|
</Callout>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Callout variant="warning" class="m-4">
|
||||||
|
<CalloutTitle>You are watching cached statistics</CalloutTitle>
|
||||||
|
<CalloutContent>
|
||||||
|
Currently there is no backup database loaded. You can only watch statistics that have been
|
||||||
|
cached, meaning only chats you already opened or chats that were preloaded.
|
||||||
|
<br />
|
||||||
|
<A href="/">Load a backup</A>
|
||||||
|
</CalloutContent>
|
||||||
|
</Callout>
|
||||||
|
</Show>
|
||||||
|
<main class="px-4 md:px-0">
|
||||||
|
<Suspense>{props.children}</Suspense>
|
||||||
|
</main>
|
||||||
|
<footer class="mt-4 flex flex-row justify-end bg-muted p-8">
|
||||||
|
<A href="/privacy">Privacy policy</A>
|
||||||
|
</footer>
|
||||||
|
</ColorModeProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Route path="/" component={Home} />
|
||||||
|
<Route path="/overview" component={Overview} />
|
||||||
|
<Route path="/dm/:dmid" component={DmId} preload={preloadDmId} />
|
||||||
|
<Route path="/group/:groupid" component={GroupId} />
|
||||||
|
<Route path="/privacy" component={Privacy} />{" "}
|
||||||
|
</Router>
|
||||||
|
</MetaProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@
|
||||||
--secondary: 240 4.8% 95.9%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--accent: 240 4.8% 95.9%;
|
--accent: 226, 90%, 85%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--accent-foreground: 226, 90%, 57%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
--muted: 240 3.7% 15.9%;
|
--muted: 240 3.7% 15.9%;
|
||||||
--muted-foreground: 240 5% 64.9%;
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
--accent: 240 3.7% 15.9%;
|
--accent: 226, 90%, 20%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 226, 90%, 57%;
|
||||||
|
|
||||||
--popover: 240 10% 3.9%;
|
--popover: 240 10% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
260
src/components/ui/dropdown-menu.tsx
Normal file
260
src/components/ui/dropdown-menu.tsx
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js"
|
||||||
|
import { splitProps } from "solid-js"
|
||||||
|
|
||||||
|
import * as DropdownMenuPrimitive from "@kobalte/core/dropdown-menu"
|
||||||
|
import type { PolymorphicProps } from "@kobalte/core/polymorphic"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenu: Component<DropdownMenuPrimitive.DropdownMenuRootProps> = (props) => {
|
||||||
|
return <DropdownMenuPrimitive.Root gutter={4} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuContentProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuContentProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuContent = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuContentProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuContentProps, ["class"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
class={cn(
|
||||||
|
"z-50 min-w-32 origin-[var(--kb-menu-content-transform-origin)] animate-content-hide overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-content-show",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuItemProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuItemProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuItem = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuItemProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuItemProps, ["class"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
class={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuShortcut: Component<ComponentProps<"span">> = (props) => {
|
||||||
|
const [, rest] = splitProps(props, ["class"])
|
||||||
|
return <span class={cn("ml-auto text-xs tracking-widest opacity-60", props.class)} {...rest} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuLabel: Component<ComponentProps<"div"> & { inset?: boolean }> = (props) => {
|
||||||
|
const [, rest] = splitProps(props, ["class", "inset"])
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold", props.inset && "pl-8", props.class)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuSeparatorProps, ["class"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
class={cn("-mx-1 my-1 h-px bg-muted", props.class)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
children?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuSubTriggerProps, ["class", "children"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
class={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="ml-auto size-4"
|
||||||
|
>
|
||||||
|
<path d="M9 6l6 6l-6 6" />
|
||||||
|
</svg>
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuSubContentProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuSubContentProps, ["class"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
class={cn(
|
||||||
|
"z-50 min-w-32 origin-[var(--kb-menu-content-transform-origin)] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
children?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuCheckboxItemProps, ["class", "children"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
class={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path d="M5 12l5 5l10 -10" />
|
||||||
|
</svg>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{props.children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuGroupLabelProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuGroupLabelProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuGroupLabelProps, ["class"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.GroupLabel
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold", props.class)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuRadioItemProps<T extends ValidComponent = "div"> =
|
||||||
|
DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {
|
||||||
|
class?: string | undefined
|
||||||
|
children?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
|
||||||
|
props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>
|
||||||
|
) => {
|
||||||
|
const [, rest] = splitProps(props as DropdownMenuRadioItemProps, ["class", "children"])
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
class={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
props.class
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="size-2 fill-current"
|
||||||
|
>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||||
|
</svg>
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{props.children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuGroupLabel,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem
|
||||||
|
}
|
33
src/components/ui/mode-toggle.tsx
Normal file
33
src/components/ui/mode-toggle.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { useColorMode } from "@kobalte/core";
|
||||||
|
|
||||||
|
import { Button } from "./button";
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu";
|
||||||
|
import { Sun, Moon, Laptop } from "lucide-solid";
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { setColorMode } = useColorMode();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as={Button<"button">} variant="ghost" size="sm" class="w-9 px-0">
|
||||||
|
<Sun class="dark:-rotate-90 size-6 rotate-0 scale-100 transition-all dark:scale-0" />
|
||||||
|
<Moon class="absolute size-6 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span class="sr-only">Toggle theme</span>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onSelect={() => setColorMode("light")}>
|
||||||
|
<Sun class="mr-2 size-4" />
|
||||||
|
<span>Light</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => setColorMode("dark")}>
|
||||||
|
<Moon class="mr-2 size-4" />
|
||||||
|
<span>Dark</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => setColorMode("system")}>
|
||||||
|
<Laptop class="mr-2 size-4" />
|
||||||
|
<span>System</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +1,5 @@
|
||||||
/* @refresh reload */
|
import { render } from "solid-js/web";
|
||||||
import { MetaProvider } from "@solidjs/meta";
|
|
||||||
import { Router, useNavigate } from "@solidjs/router";
|
|
||||||
import { Portal, render } from "solid-js/web";
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { hasCashedData } from "./lib/db-cache";
|
|
||||||
import { createEffect, Show } from "solid-js";
|
|
||||||
import { dbLoaded } from "./db";
|
|
||||||
import { Callout, CalloutContent, CalloutTitle } from "./components/ui/callout";
|
|
||||||
import { A } from "./components/ui/A";
|
|
||||||
import { isWasmSupported } from "./lib/utils";
|
|
||||||
|
|
||||||
const root = document.getElementById("root");
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
@ -26,82 +17,6 @@ if (import.meta.env.DEV && !("umami" in window)) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const NO_DATA_NEEDED_PAGES = ["/", "/privacy"];
|
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
render(
|
render(() => <App />, root);
|
||||||
() => (
|
|
||||||
<div class="mx-auto max-w-(--breakpoint-2xl)">
|
|
||||||
<MetaProvider>
|
|
||||||
<Router
|
|
||||||
root={(props) => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!dbLoaded() && !hasCashedData() && !NO_DATA_NEEDED_PAGES.includes(props.location.pathname)) {
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const wasmSupport = isWasmSupported();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Show when={!wasmSupport}>
|
|
||||||
<Portal>
|
|
||||||
<div class="fixed inset-0 mx-4 flex flex-col items-center justify-center backdrop-blur-lg">
|
|
||||||
<Callout variant="error">
|
|
||||||
Your browser does not support WebAssembly, which is required for this site to work with the
|
|
||||||
big amount of data a signal backup contains.
|
|
||||||
<br />
|
|
||||||
Please try a different browser.
|
|
||||||
</Callout>
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={props.location.pathname !== "/" && !dbLoaded() && hasCashedData()}
|
|
||||||
fallback={
|
|
||||||
<Show when={!dbLoaded() && hasCashedData()}>
|
|
||||||
<Callout variant="default" class="m-4">
|
|
||||||
There is currently no backup database loaded, but you can watch statistics that have been
|
|
||||||
cached, meaning only chats you already opened or chats that were preloaded.
|
|
||||||
<br />
|
|
||||||
<A
|
|
||||||
href="/overview"
|
|
||||||
onClick={() => {
|
|
||||||
umami.track("Watch cached statistics");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Watch cached statistics
|
|
||||||
</A>
|
|
||||||
</Callout>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Callout variant="warning" class="m-4">
|
|
||||||
<CalloutTitle>You are watching cached statistics</CalloutTitle>
|
|
||||||
<CalloutContent>
|
|
||||||
Currently there is no backup database loaded. You can only watch statistics that have been
|
|
||||||
cached, meaning only chats you already opened or chats that were preloaded.
|
|
||||||
<br />
|
|
||||||
<A href="/">Load a backup</A>
|
|
||||||
</CalloutContent>
|
|
||||||
</Callout>
|
|
||||||
</Show>
|
|
||||||
<main>{props.children}</main>
|
|
||||||
<footer class="mt-4 flex flex-row justify-end bg-muted p-8">
|
|
||||||
<A href="/privacy">Privacy policy</A>
|
|
||||||
</footer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<App />
|
|
||||||
</Router>
|
|
||||||
</MetaProvider>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
root,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ export const DmMessagesPerDate: Component<{
|
||||||
label: "Total",
|
label: "Total",
|
||||||
data: currentDmMessagesValues.map((row) => row.totalMessages),
|
data: currentDmMessagesValues.map((row) => row.totalMessages),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHitRadius: 6,
|
||||||
},
|
},
|
||||||
...currentRecipients.map((recipient) => {
|
...currentRecipients.map((recipient) => {
|
||||||
return {
|
return {
|
||||||
|
@ -28,6 +30,8 @@ export const DmMessagesPerDate: Component<{
|
||||||
label: recipient.name.toString(),
|
label: recipient.name.toString(),
|
||||||
data: currentDmMessagesValues.map((date) => date[recipient.recipientId]),
|
data: currentDmMessagesValues.map((date) => date[recipient.recipientId]),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHitRadius: 6,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -41,7 +45,7 @@ export const DmMessagesPerDate: Component<{
|
||||||
<LineChart
|
<LineChart
|
||||||
options={{
|
options={{
|
||||||
normalized: true,
|
normalized: true,
|
||||||
aspectRatio: 3,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: {
|
pan: {
|
||||||
|
@ -61,6 +65,7 @@ export const DmMessagesPerDate: Component<{
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
data={currentDateChartData()}
|
data={currentDateChartData()}
|
||||||
|
class="max-h-96"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const DmOverview: Component<{
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid cols={1} colsMd={2} class="my-12 min-w-[35rem] gap-y-8 text-sm">
|
<Grid cols={1} colsMd={2} class="my-12 w-full gap-y-8 text-sm md:min-w-[35rem]">
|
||||||
<Flex flexDirection="row" justifyContent="evenly" class="bg-amber-200 p-2 text-amber-900">
|
<Flex flexDirection="row" justifyContent="evenly" class="bg-amber-200 p-2 text-amber-900">
|
||||||
<Flex alignItems="center" justifyContent="center" class="min-w-16">
|
<Flex alignItems="center" justifyContent="center" class="min-w-16">
|
||||||
<CalendarArrowDown class="h-8 w-8" />
|
<CalendarArrowDown class="h-8 w-8" />
|
||||||
|
|
|
@ -122,16 +122,16 @@ export const Home: Component<RouteSectionProps> = () => {
|
||||||
</Flex>
|
</Flex>
|
||||||
</Portal>
|
</Portal>
|
||||||
<Title>Signal stats</Title>
|
<Title>Signal stats</Title>
|
||||||
<form class="flex flex-col gap-y-8 p-8" onSubmit={onSubmit}>
|
<form class="mx-auto flex w-full flex-col gap-y-8 p-8 md:w-fit" onSubmit={onSubmit}>
|
||||||
<TextField onChange={(value) => setPassphrase(value)}>
|
<TextField onChange={(value) => setPassphrase(value)}>
|
||||||
<TextFieldLabel>Passphrase</TextFieldLabel>
|
<TextFieldLabel>Passphrase</TextFieldLabel>
|
||||||
<TextFieldInput type="password" class="max-w-md" />
|
<TextFieldInput type="password" class="w-full md:w-sm" />
|
||||||
</TextField>
|
</TextField>
|
||||||
<Flex
|
<Flex
|
||||||
ref={dropzone.setRef}
|
ref={dropzone.setRef}
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
class="relative min-h-32 min-w-96 max-w-xl rounded-lg border-4 border-border border-dashed"
|
class="relative min-h-40 w-full rounded-lg border-4 border-border border-dashed md:w-xl"
|
||||||
classList={{
|
classList={{
|
||||||
"border-ring": dropzone.isDragging(),
|
"border-ring": dropzone.isDragging(),
|
||||||
}}
|
}}
|
||||||
|
@ -154,7 +154,7 @@ export const Home: Component<RouteSectionProps> = () => {
|
||||||
{backupFile() ? backupFile()?.name : "or drop the file here"}
|
{backupFile() ? backupFile()?.name : "or drop the file here"}
|
||||||
</span>
|
</span>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button type="submit" class="max-w-72">
|
<Button type="submit" class="max-w-72 self-end md:w-sm">
|
||||||
Decrypt and load backup
|
Decrypt and load backup
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue