Functional Login Page

This commit is contained in:
Neshura 2024-04-21 02:15:50 +02:00
parent f40f5c2d20
commit 59bedf73da
Signed by: Neshura
GPG key ID: B6983AAA6B9A7A6C
40 changed files with 8326 additions and 40 deletions

14
components.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "new-york",
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app.pcss",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

4864
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,29 +1,48 @@
{ {
"name": "navidrome-alternate-ui", "name": "navidrome-alternate-ui",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint ." "lint": "eslint .",
}, "ui": "npx shadcn-svelte@latest"
"devDependencies": { },
"@sveltejs/adapter-auto": "^3.0.0", "devDependencies": {
"@sveltejs/kit": "^2.0.0", "@sveltejs/adapter-auto": "^3.2.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/kit": "^2.5.7",
"@types/eslint": "^8.56.0", "@sveltejs/vite-plugin-svelte": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@types/eslint": "^8.56.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"eslint": "^8.56.0", "@typescript-eslint/parser": "^7.0.0",
"eslint-plugin-svelte": "^2.36.0-next.4", "autoprefixer": "^10.4.16",
"svelte": "^5.0.0-next.1", "eslint": "^8.56.0",
"svelte-check": "^3.6.0", "eslint-plugin-svelte": "^2.36.0-next.13",
"tslib": "^2.4.1", "postcss": "^8.4.32",
"typescript": "^5.0.0", "postcss-load-config": "^5.0.2",
"vite": "^5.0.3" "svelte": "^5.0.0-next",
}, "svelte-check": "^3.6.0",
"type": "module" "svelte-loading-spinners": "^0.3.6",
"tailwindcss": "^3.3.6",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"@radix-ui/react-icons": "^1.3.0",
"bits-ui": "^0.21.4",
"clsx": "^2.1.0",
"formsnap": "^1.0.0",
"js-cookie": "^3.0.5",
"radix-icons-svelte": "^1.2.1",
"sveltekit-superforms": "^2.12.5",
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
"ts-md5": "^1.3.1",
"zod": "^3.22.5"
}
} }

13
postcss.config.cjs Normal file
View file

@ -0,0 +1,13 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer,
],
};
module.exports = config;

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover" class="h-screen light dark">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

59
src/app.pcss Normal file
View file

@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 191.6 100% 95%;
--foreground: 191.6 5% 0%;
--card: 191.6 50% 90%;
--card-foreground: 191.6 5% 10%;
--popover: 191.6 100% 95%;
--popover-foreground: 191.6 100% 0%;
--primary: 191.6 91.4% 36.5%;
--primary-foreground: 0 0% 100%;
--secondary: 191.6 30% 70%;
--secondary-foreground: 0 0% 0%;
--muted: 153.6 30% 85%;
--muted-foreground: 191.6 5% 35%;
--accent: 153.6 30% 80%;
--accent-foreground: 191.6 5% 10%;
--destructive: 0 100% 30%;
--destructive-foreground: 191.6 5% 90%;
--border: 191.6 30% 50%;
--input: 191.6 30% 18%;
--ring: 191.6 91.4% 36.5%;
--radius: 0.75rem;
}
.dark {
--background: 191.6 50% 5%;
--foreground: 191.6 5% 90%;
--card: 191.6 50% 0%;
--card-foreground: 191.6 5% 90%;
--popover: 191.6 50% 5%;
--popover-foreground: 191.6 5% 90%;
--primary: 191.6 91.4% 36.5%;
--primary-foreground: 0 0% 100%;
--secondary: 191.6 30% 10%;
--secondary-foreground: 0 0% 100%;
--muted: 153.6 30% 15%;
--muted-foreground: 191.6 5% 60%;
--accent: 153.6 30% 15%;
--accent-foreground: 191.6 5% 90%;
--destructive: 0 100% 30%;
--destructive-foreground: 191.6 5% 90%;
--border: 191.6 30% 18%;
--input: 191.6 30% 18%;
--ring: 191.6 91.4% 36.5%;
--radius: 0.75rem;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<p class={cn("text-sm text-muted-foreground", className)} {...$$restProps}>
<slot />
</p>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 p-6", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let tag: $$Props["tag"] = "h3";
export { className as class };
</script>
<svelte:element
this={tag}
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,24 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
};
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";

View file

@ -0,0 +1,10 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>

View file

@ -0,0 +1,25 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

View file

@ -0,0 +1,25 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

View file

@ -0,0 +1,30 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { cn } from "$lib/utils.js";
import { Label } from "$lib/components/ui/label/index.js";
type $$Props = LabelPrimitive.Props;
let className: $$Props["class"] = undefined;
export { className as class };
const { labelAttrs } = getFormControl();
</script>
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

View file

@ -0,0 +1,33 @@
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
const Control = FormPrimitive.Control;
export {
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
};

View file

@ -0,0 +1,28 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,41 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
on:wheel
{...$$restProps}
/>

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

52
src/lib/opensubsonic.ts Normal file
View file

@ -0,0 +1,52 @@
import {Md5} from "ts-md5";
import Cookies from 'js-cookie';
export module OpenSubsonic {
export let username = "";
export let password = "";
let token = "";
let salt = "";
export let base = "https://navidrome.neshweb.net";
const apiVer = "1.16.1"; // Version supported by Navidrome. Variable for easier updating
const clientName = "Lytter";
export function getApiPath(path: string) {
if (username === "") {
let cookie = Cookies.get("subsonicUsername");
if (typeof cookie !== "undefined") {
username = cookie;
}
}
let cookie = Cookies.get("subsonicToken");
if (typeof cookie !== "undefined") {
token = cookie;
cookie = Cookies.get("subsonicSalt");
if (typeof cookie !== "undefined") {
salt = cookie;
}
}
else {
generateToken();
}
return `${base}/rest/${path}?u=${username}&s=${salt}&t=${token}&v=${apiVer}&c=${clientName}&f=json`;
}
function generateToken() {
salt = "staticfornow";
token = Md5.hashStr(password + salt);
}
export function storeCredentials() {
let options = {
expires: 7,
path: "/",
sameSite: "strict",
};
Cookies.set("subsonicToken", token, options);
Cookies.set("subsonicSalt", salt, options);
Cookies.set("subsonicUsername", username, options);
}
}

62
src/lib/utils.ts Normal file
View file

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View file

@ -0,0 +1,13 @@
import {redirect} from "@sveltejs/kit";
export function load({ cookies, url }) {
let auth = cookies.get("subsonicToken");
if (typeof auth === "undefined" && url.pathname !== "/login") {
cookies.set("preLoginRoute", `${url.pathname}`, {
path: "/login",
secure: false,
httpOnly: false,
});
throw redirect(302, '/login');
}
}

View file

@ -0,0 +1,5 @@
<script>
import "../app.pcss";
</script>
<slot></slot>

View file

@ -0,0 +1,26 @@
import type { PageServerLoad, Actions } from "./$types.js";
import { fail } from "@sveltejs/kit";
import { superValidate } from "sveltekit-superforms";
import { formSchema } from "./schema";
import { zod } from "sveltekit-superforms/adapters";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(zod(formSchema)),
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(formSchema));
if (!form.valid) {
return fail(400, {
form,
});
}
return {
form,
};
},
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import LoginForm from "./login-form.svelte";
import type { PageData } from "./$types.js";
export let data: PageData;
</script>
<div class="h-full flex flex-col items-center justify-center">
<Card.Root class="border-2 p-2">
<Card.Header>
<Card.Title class="text-center">Login</Card.Title>
<Card.Description></Card.Description>
</Card.Header>
<Card.Content>
<LoginForm data={data.form}></LoginForm>
</Card.Content>
<Card.Footer>
<p class="text-xs text-secondary pt-4">Sign Up (not implemented)</p>
</Card.Footer>
</Card.Root>
</div>

View file

@ -0,0 +1,85 @@
<svelte:options runes={true} />
<script lang="ts">
import { Input } from "$lib/components/ui/input";
import * as Form from "$lib/components/ui/form";
import { formSchema, type FormSchema } from "./schema";
import {OpenSubsonic} from "$lib/opensubsonic";
import {
type SuperValidated,
type Infer,
superForm,
} from "sveltekit-superforms";
import { zodClient } from "sveltekit-superforms/adapters";
import {Circle2} from "svelte-loading-spinners";
import {Md5} from "ts-md5";
import Cookies from 'js-cookie'
import {goto} from "$app/navigation";
let { data }: SuperValidated<Infer<FormSchema>> = $props<{ data: SuperValidated<Infer<FormSchema>> }>();
const form = superForm(data, {
validators: zodClient(formSchema),
onUpdated() {
OpenSubsonic.username = previousForm.get("username");
OpenSubsonic.password = previousForm.get("password");
let path = OpenSubsonic.getApiPath("ping");
fetch(path).then(async (data) => {
let res = (await data.json())["subsonic-response"];
if (res.status === "ok") {
OpenSubsonic.storeCredentials();
loading = false;
let route = Cookies.get("preLoginRoute")
Cookies.remove("preLoginRoute", { path: "/login" })
goto(route);
}
else {
error = true;
loading = false;
}
});
},
onSubmit({ formData }) {
loading = true;
previousForm = formData;
}
});
let previousForm: FormData = $state({});
const { form: formData, enhance } = form;
let error = $state(false);
let loading = $state(false);
</script>
<form method="POST" use:enhance>
<Form.Field {form} name="username" class="px-3">
<Form.Control let:attrs>
<Form.Label>Username:</Form.Label>
<Input {...attrs} bind:value={$formData.username}></Input>
</Form.Control>
<Form.FieldErrors></Form.FieldErrors>
</Form.Field>
<Form.Field {form} name="password" class="px-3">
<Form.Control let:attrs>
<Form.Label>Password:</Form.Label>
<Input {...attrs} type="password" bind:value={$formData.password}></Input>
</Form.Control>
<Form.FieldErrors></Form.FieldErrors>
</Form.Field>
<div class="flex flex-row justify-center pt-2">
{#if loading}
<Circle2
size="40" unit="px"
colorOuter="#0892B4" durationOuter="1s"
colorCenter="white" durationCenter="3s"
colorInner="#9cc1c9" durationInner="2s"
/>
{:else}
<Form.Button>Login</Form.Button>
{/if}
</div>
{#if error}
<p class="pt-2 text-sm text-destructive">Username or Password incorrect</p>
{/if}
</form>

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const formSchema = z.object({
username: z.string().min(2).max(64),
password: z.string().min(2).max(64),
});
export type FormSchema = typeof formSchema;

View file

@ -1,18 +1,18 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: [vitePreprocess({})],
kit: { kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter. // If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() adapter: adapter(),
} },
}; };
export default config; export default config;

64
tailwind.config.js Normal file
View file

@ -0,0 +1,64 @@
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
},
};
export default config;

View file

@ -2,5 +2,8 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit()],
server: {
host: '::'
}
}); });

2492
yarn.lock Normal file

File diff suppressed because it is too large Load diff