first commit
This commit is contained in:
155
apps/web/src/lib/components/CreateHostForm.svelte
Normal file
155
apps/web/src/lib/components/CreateHostForm.svelte
Normal file
@@ -0,0 +1,155 @@
|
||||
<script lang="ts">
|
||||
import { superForm, defaults } from 'sveltekit-superforms';
|
||||
import SuperDebug from 'sveltekit-superforms';
|
||||
import { zodClient, zod } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import * as Form from '$lib/components/ui/form/index.js';
|
||||
import { Input } from './ui/input';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { cn } from '$lib/utils';
|
||||
import { pb } from '$lib/pb';
|
||||
import { dev } from '$app/environment';
|
||||
import { hostsStore } from '$lib/stores/hosts';
|
||||
import * as Popover from '$lib/components/ui/popover/index';
|
||||
import { Button } from './ui/button';
|
||||
import { Icon, InfoIcon } from 'lucide-svelte';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
mac: z
|
||||
.string()
|
||||
.min(1, 'MAC is required')
|
||||
.regex(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, 'Invalid MAC address format'),
|
||||
ip: z.string().min(1, 'IP is required').ip(),
|
||||
port: z.number().default(9)
|
||||
});
|
||||
|
||||
const form = superForm(defaults(zod(formSchema)), {
|
||||
validators: zodClient(formSchema),
|
||||
SPA: true,
|
||||
onUpdate({ form, cancel }) {
|
||||
if (!form.valid) {
|
||||
toast.error('Invalid');
|
||||
return;
|
||||
}
|
||||
if (!pb.authStore.record) {
|
||||
toast.error('You must be logged in to create a host');
|
||||
return;
|
||||
}
|
||||
hostsStore.createHost({ ...form.data, user: pb.authStore.record?.id });
|
||||
toast.success('Host created');
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
const { form: formData, enhance, errors } = form;
|
||||
|
||||
let { class: className }: { class?: string } = $props();
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance class={cn('grid grid-cols-1 md:grid-cols-2 gap-4', className)}>
|
||||
<Form.Field {form} name="name">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<div class="space-y-2">
|
||||
<Form.Label class="text-sm font-medium">Device Name</Form.Label>
|
||||
<Input
|
||||
{...props}
|
||||
name="name"
|
||||
bind:value={$formData.name}
|
||||
placeholder="e.g., TrueNAS, Gaming PC"
|
||||
class="h-11"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors class="text-xs" />
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field {form} name="mac">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<div class="space-y-2">
|
||||
<Form.Label class="text-sm font-medium">MAC Address</Form.Label>
|
||||
<Input
|
||||
{...props}
|
||||
name="mac"
|
||||
bind:value={$formData.mac}
|
||||
placeholder="86:2f:57:c1:df:65"
|
||||
class="h-11 font-mono"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors class="text-xs" />
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field {form} name="ip">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<div class="space-y-2">
|
||||
<Form.Label class="text-sm font-medium">Broadcast IP</Form.Label>
|
||||
<div class="flex space-x-2">
|
||||
<Input
|
||||
{...props}
|
||||
name="ip"
|
||||
bind:value={$formData.ip}
|
||||
placeholder="255.255.255.255"
|
||||
class="h-11 font-mono flex-1"
|
||||
/>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="secondary" size="icon" class="h-11 w-11 flex-shrink-0">
|
||||
<InfoIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-80 glass-effect">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-semibold text-sm">Broadcast IP Guide</h4>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Use a broadcast IP address. Default is <code class="px-1 py-0.5 bg-muted rounded text-xs">255.255.255.255</code>
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
For multiple networks, use a subnet-specific broadcast IP.
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Example: If host IP is <code class="px-1 py-0.5 bg-muted rounded text-xs">192.168.1.123</code>, use <code class="px-1 py-0.5 bg-muted rounded text-xs">192.168.1.255</code>
|
||||
</p>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors class="text-xs" />
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field {form} name="port">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<div class="space-y-2">
|
||||
<Form.Label class="text-sm font-medium">Port</Form.Label>
|
||||
<Input
|
||||
{...props}
|
||||
name="port"
|
||||
bind:value={$formData.port}
|
||||
placeholder="9"
|
||||
class="h-11 font-mono"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.FieldErrors class="text-xs" />
|
||||
</Form.Field>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<Form.Button class="w-full h-11 bg-primary hover:bg-primary/90 font-semibold">
|
||||
Add Device
|
||||
</Form.Button>
|
||||
</div>
|
||||
|
||||
{#if dev}
|
||||
<!-- <SuperDebug data={$formData} /> -->
|
||||
{/if}
|
||||
</form>
|
||||
85
apps/web/src/lib/components/HostCard.svelte
Normal file
85
apps/web/src/lib/components/HostCard.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import type { HostsRecord } from '$lib/pocketbase-types';
|
||||
import { cn } from '$lib/utils';
|
||||
import { Trash2, Zap, Network, Hash, Radio } from 'lucide-svelte';
|
||||
import { Button } from './ui/button';
|
||||
import { hostsStore } from '$lib/stores/hosts';
|
||||
|
||||
let { host, class: className }: { host: HostsRecord; class?: string } = $props();
|
||||
|
||||
function wake() {
|
||||
hostsStore.wakeHost(host);
|
||||
}
|
||||
|
||||
function deleteHost() {
|
||||
if (window.confirm(`Are you sure you want to delete "${host.name}"?`)) {
|
||||
hostsStore.deleteHost(host);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class={cn('relative overflow-hidden group hover:shadow-xl transition-all duration-300 glass-effect', className)}>
|
||||
<!-- Gradient accent on hover -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
|
||||
|
||||
<Card.Header class="pb-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center glow-primary">
|
||||
<Network class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">{host.name}</h3>
|
||||
<p class="text-sm text-muted-foreground">Network Device</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
|
||||
onclick={deleteHost}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="space-y-3">
|
||||
<div class="grid gap-3">
|
||||
<div class="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
|
||||
<Hash class="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs text-muted-foreground">MAC Address</p>
|
||||
<p class="font-mono text-sm font-medium truncate">{host.mac}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
|
||||
<Radio class="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs text-muted-foreground">Broadcast IP</p>
|
||||
<p class="font-mono text-sm font-medium truncate">{host.ip}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
|
||||
<Network class="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs text-muted-foreground">Port</p>
|
||||
<p class="font-mono text-sm font-medium">{host.port}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Footer class="pt-4">
|
||||
<Button
|
||||
class="w-full bg-gradient-to-r from-green-600 to-green-500 hover:from-green-500 hover:to-green-400 text-white font-semibold shadow-lg glow-success transition-all duration-300 transform hover:scale-[1.02]"
|
||||
onclick={wake}
|
||||
>
|
||||
<Zap class="h-4 w-4 mr-2" />
|
||||
Wake Device
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
84
apps/web/src/lib/components/navbar.svelte
Normal file
84
apps/web/src/lib/components/navbar.svelte
Normal file
@@ -0,0 +1,84 @@
|
||||
<script>
|
||||
import { Button } from './ui/button';
|
||||
import { pb } from '$lib/pb';
|
||||
import { cn } from '$lib/utils';
|
||||
import { page } from '$app/state';
|
||||
import { LogOut, Zap, Home, Lock } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
function logout() {
|
||||
pb.authStore.clear();
|
||||
goto('/auth');
|
||||
}
|
||||
|
||||
let isAuth = $state(pb.authStore.isValid);
|
||||
|
||||
$effect(() => {
|
||||
page.url.pathname;
|
||||
isAuth = pb.authStore.isValid;
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="fixed top-0 w-full z-50 backdrop-blur-xl bg-background/70 border-b border-border/50"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 glow-primary">
|
||||
<Zap class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-lg leading-none">WoL Manager</span>
|
||||
<span class="text-xs text-muted-foreground leading-none">Wake-on-LAN</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="flex items-center space-x-1">
|
||||
<a
|
||||
href="/"
|
||||
class={cn(
|
||||
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200',
|
||||
page.url.pathname === '/'
|
||||
? 'bg-primary text-primary-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Home class="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a
|
||||
href="/auth"
|
||||
class={cn(
|
||||
'flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200',
|
||||
page.url.pathname === '/auth'
|
||||
? 'bg-primary text-primary-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Lock class="h-4 w-4" />
|
||||
<span>Auth</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- User Actions -->
|
||||
<div>
|
||||
{#if isAuth}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="group hover:border-destructive hover:text-destructive transition-colors"
|
||||
onclick={logout}
|
||||
>
|
||||
<LogOut class="h-4 w-4 mr-2" />
|
||||
<span class="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="w-20"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
74
apps/web/src/lib/components/ui/button/button.svelte
Normal file
74
apps/web/src/lib/components/ui/button/button.svelte
Normal file
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" module>
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
17
apps/web/src/lib/components/ui/button/index.ts
Normal file
17
apps/web/src/lib/components/ui/button/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
type ButtonProps as Props,
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
};
|
||||
16
apps/web/src/lib/components/ui/card/card-content.svelte
Normal file
16
apps/web/src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
16
apps/web/src/lib/components/ui/card/card-description.svelte
Normal file
16
apps/web/src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
16
apps/web/src/lib/components/ui/card/card-footer.svelte
Normal file
16
apps/web/src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
16
apps/web/src/lib/components/ui/card/card-header.svelte
Normal file
16
apps/web/src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
apps/web/src/lib/components/ui/card/card-title.svelte
Normal file
25
apps/web/src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
level = 3,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="heading"
|
||||
aria-level={level}
|
||||
bind:this={ref}
|
||||
class={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
apps/web/src/lib/components/ui/card/card.svelte
Normal file
20
apps/web/src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
22
apps/web/src/lib/components/ui/card/index.ts
Normal file
22
apps/web/src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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,
|
||||
};
|
||||
7
apps/web/src/lib/components/ui/form/form-button.svelte
Normal file
7
apps/web/src/lib/components/ui/form/form-button.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as Button from "$lib/components/ui/button/index.js";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: Button.Props = $props();
|
||||
</script>
|
||||
|
||||
<Button.Root bind:ref type="submit" {...restProps} />
|
||||
17
apps/web/src/lib/components/ui/form/form-description.svelte
Normal file
17
apps/web/src/lib/components/ui/form/form-description.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import * as FormPrimitive from "formsnap";
|
||||
import type { WithoutChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChild<FormPrimitive.DescriptionProps> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Description
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" module>
|
||||
import type { FormPathLeaves as _FormPathLeaves } 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 * as FormPrimitive from "formsnap";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef, WithoutChildren } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
form,
|
||||
name,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> &
|
||||
FormPrimitive.ElementFieldProps<T, U> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.ElementField {form} {name}>
|
||||
{#snippet children({ constraints, errors, tainted, value })}
|
||||
<div bind:this={ref} class={cn("space-y-2", className)} {...restProps}>
|
||||
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
|
||||
</div>
|
||||
{/snippet}
|
||||
</FormPrimitive.ElementField>
|
||||
31
apps/web/src/lib/components/ui/form/form-field-errors.svelte
Normal file
31
apps/web/src/lib/components/ui/form/form-field-errors.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import * as FormPrimitive from "formsnap";
|
||||
import type { WithoutChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
errorClasses,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<FormPrimitive.FieldErrorsProps> & {
|
||||
errorClasses?: string | undefined | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.FieldErrors
|
||||
bind:ref
|
||||
class={cn("text-destructive text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ errors, errorProps })}
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ errors, errorProps })}
|
||||
{:else}
|
||||
{#each errors as error}
|
||||
<div {...errorProps} class={cn(errorClasses)}>{error}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FormPrimitive.FieldErrors>
|
||||
30
apps/web/src/lib/components/ui/form/form-field.svelte
Normal file
30
apps/web/src/lib/components/ui/form/form-field.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" module>
|
||||
import type { FormPath as _FormPath } 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 type { WithoutChildren, WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
form,
|
||||
name,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: FormPrimitive.FieldProps<T, U> &
|
||||
WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Field {form} {name}>
|
||||
{#snippet children({ constraints, errors, tainted, value })}
|
||||
<div bind:this={ref} class={cn("space-y-2", className)} {...restProps}>
|
||||
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
|
||||
</div>
|
||||
{/snippet}
|
||||
</FormPrimitive.Field>
|
||||
21
apps/web/src/lib/components/ui/form/form-fieldset.svelte
Normal file
21
apps/web/src/lib/components/ui/form/form-fieldset.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" module>
|
||||
import type { FormPath as _FormPath } 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 type { WithoutChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
form,
|
||||
name,
|
||||
...restProps
|
||||
}: WithoutChild<FormPrimitive.FieldsetProps<T, U>> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Fieldset bind:ref {form} {name} class={cn("space-y-2", className)} {...restProps} />
|
||||
21
apps/web/src/lib/components/ui/form/form-label.svelte
Normal file
21
apps/web/src/lib/components/ui/form/form-label.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { WithoutChild } from "bits-ui";
|
||||
import * as FormPrimitive from "formsnap";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChild<FormPrimitive.LabelProps> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Label {...restProps} bind:ref>
|
||||
{#snippet child({ props })}
|
||||
<Label {...props} class={cn("data-[fs-error]:text-destructive", className)}>
|
||||
{@render children?.()}
|
||||
</Label>
|
||||
{/snippet}
|
||||
</FormPrimitive.Label>
|
||||
17
apps/web/src/lib/components/ui/form/form-legend.svelte
Normal file
17
apps/web/src/lib/components/ui/form/form-legend.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import * as FormPrimitive from "formsnap";
|
||||
import type { WithoutChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChild<FormPrimitive.LegendProps> = $props();
|
||||
</script>
|
||||
|
||||
<FormPrimitive.Legend
|
||||
bind:ref
|
||||
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
33
apps/web/src/lib/components/ui/form/index.ts
Normal file
33
apps/web/src/lib/components/ui/form/index.ts
Normal 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,
|
||||
};
|
||||
7
apps/web/src/lib/components/ui/input/index.ts
Normal file
7
apps/web/src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
22
apps/web/src/lib/components/ui/input/input.svelte
Normal file
22
apps/web/src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLInputAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
7
apps/web/src/lib/components/ui/label/index.ts
Normal file
7
apps/web/src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
19
apps/web/src/lib/components/ui/label/label.svelte
Normal file
19
apps/web/src/lib/components/ui/label/label.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
17
apps/web/src/lib/components/ui/popover/index.ts
Normal file
17
apps/web/src/lib/components/ui/popover/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Trigger = PopoverPrimitive.Trigger;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: PopoverPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
7
apps/web/src/lib/components/ui/separator/index.ts
Normal file
7
apps/web/src/lib/components/ui/separator/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
};
|
||||
22
apps/web/src/lib/components/ui/separator/separator.svelte
Normal file
22
apps/web/src/lib/components/ui/separator/separator.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "horizontal",
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-border shrink-0",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
{...restProps}
|
||||
/>
|
||||
1
apps/web/src/lib/components/ui/sonner/index.ts
Normal file
1
apps/web/src/lib/components/ui/sonner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
20
apps/web/src/lib/components/ui/sonner/sonner.svelte
Normal file
20
apps/web/src/lib/components/ui/sonner/sonner.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let restProps: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={$mode}
|
||||
class="toaster group"
|
||||
toastOptions={{
|
||||
classes: {
|
||||
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
1
apps/web/src/lib/index.ts
Normal file
1
apps/web/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
5
apps/web/src/lib/pb.ts
Normal file
5
apps/web/src/lib/pb.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
import type { TypedPocketBase } from './pocketbase-types';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
export const pb = new PocketBase(dev ? 'http://localhost:8090' : undefined) as TypedPocketBase;
|
||||
155
apps/web/src/lib/pocketbase-types.ts
Normal file
155
apps/web/src/lib/pocketbase-types.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* This file was @generated using pocketbase-typegen
|
||||
*/
|
||||
|
||||
import type PocketBase from 'pocketbase'
|
||||
import type { RecordService } from 'pocketbase'
|
||||
|
||||
export enum Collections {
|
||||
Authorigins = "_authOrigins",
|
||||
Externalauths = "_externalAuths",
|
||||
Mfas = "_mfas",
|
||||
Otps = "_otps",
|
||||
Superusers = "_superusers",
|
||||
Hosts = "hosts",
|
||||
Users = "users",
|
||||
}
|
||||
|
||||
// Alias types for improved usability
|
||||
export type IsoDateString = string
|
||||
export type RecordIdString = string
|
||||
export type HTMLString = string
|
||||
|
||||
// System fields
|
||||
export type BaseSystemFields<T = never> = {
|
||||
id: RecordIdString
|
||||
collectionId: string
|
||||
collectionName: Collections
|
||||
expand?: T
|
||||
}
|
||||
|
||||
export type AuthSystemFields<T = never> = {
|
||||
email: string
|
||||
emailVisibility: boolean
|
||||
username: string
|
||||
verified: boolean
|
||||
} & BaseSystemFields<T>
|
||||
|
||||
// Record types for each collection
|
||||
|
||||
export type AuthoriginsRecord = {
|
||||
collectionRef: string
|
||||
created?: IsoDateString
|
||||
fingerprint: string
|
||||
id: string
|
||||
recordRef: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type ExternalauthsRecord = {
|
||||
collectionRef: string
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
provider: string
|
||||
providerId: string
|
||||
recordRef: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type MfasRecord = {
|
||||
collectionRef: string
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
method: string
|
||||
recordRef: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type OtpsRecord = {
|
||||
collectionRef: string
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
password: string
|
||||
recordRef: string
|
||||
sentTo?: string
|
||||
updated?: IsoDateString
|
||||
}
|
||||
|
||||
export type SuperusersRecord = {
|
||||
created?: IsoDateString
|
||||
email: string
|
||||
emailVisibility?: boolean
|
||||
id: string
|
||||
password: string
|
||||
tokenKey: string
|
||||
updated?: IsoDateString
|
||||
verified?: boolean
|
||||
}
|
||||
|
||||
export type HostsRecord = {
|
||||
created?: IsoDateString
|
||||
id: string
|
||||
ip: string
|
||||
mac: string
|
||||
name: string
|
||||
port: number
|
||||
updated?: IsoDateString
|
||||
user: RecordIdString
|
||||
}
|
||||
|
||||
export type UsersRecord = {
|
||||
avatar?: string
|
||||
created?: IsoDateString
|
||||
email: string
|
||||
emailVisibility?: boolean
|
||||
id: string
|
||||
name?: string
|
||||
password: string
|
||||
tokenKey: string
|
||||
updated?: IsoDateString
|
||||
verified?: boolean
|
||||
}
|
||||
|
||||
// Response types include system fields and match responses from the PocketBase API
|
||||
export type AuthoriginsResponse<Texpand = unknown> = Required<AuthoriginsRecord> & BaseSystemFields<Texpand>
|
||||
export type ExternalauthsResponse<Texpand = unknown> = Required<ExternalauthsRecord> & BaseSystemFields<Texpand>
|
||||
export type MfasResponse<Texpand = unknown> = Required<MfasRecord> & BaseSystemFields<Texpand>
|
||||
export type OtpsResponse<Texpand = unknown> = Required<OtpsRecord> & BaseSystemFields<Texpand>
|
||||
export type SuperusersResponse<Texpand = unknown> = Required<SuperusersRecord> & AuthSystemFields<Texpand>
|
||||
export type HostsResponse<Texpand = unknown> = Required<HostsRecord> & BaseSystemFields<Texpand>
|
||||
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
|
||||
|
||||
// Types containing all Records and Responses, useful for creating typing helper functions
|
||||
|
||||
export type CollectionRecords = {
|
||||
_authOrigins: AuthoriginsRecord
|
||||
_externalAuths: ExternalauthsRecord
|
||||
_mfas: MfasRecord
|
||||
_otps: OtpsRecord
|
||||
_superusers: SuperusersRecord
|
||||
hosts: HostsRecord
|
||||
users: UsersRecord
|
||||
}
|
||||
|
||||
export type CollectionResponses = {
|
||||
_authOrigins: AuthoriginsResponse
|
||||
_externalAuths: ExternalauthsResponse
|
||||
_mfas: MfasResponse
|
||||
_otps: OtpsResponse
|
||||
_superusers: SuperusersResponse
|
||||
hosts: HostsResponse
|
||||
users: UsersResponse
|
||||
}
|
||||
|
||||
// Type for usage with type asserted PocketBase instance
|
||||
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
|
||||
|
||||
export type TypedPocketBase = PocketBase & {
|
||||
collection(idOrName: '_authOrigins'): RecordService<AuthoriginsResponse>
|
||||
collection(idOrName: '_externalAuths'): RecordService<ExternalauthsResponse>
|
||||
collection(idOrName: '_mfas'): RecordService<MfasResponse>
|
||||
collection(idOrName: '_otps'): RecordService<OtpsResponse>
|
||||
collection(idOrName: '_superusers'): RecordService<SuperusersResponse>
|
||||
collection(idOrName: 'hosts'): RecordService<HostsResponse>
|
||||
collection(idOrName: 'users'): RecordService<UsersResponse>
|
||||
}
|
||||
64
apps/web/src/lib/stores/hosts.ts
Normal file
64
apps/web/src/lib/stores/hosts.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { pb } from '$lib/pb';
|
||||
import type { HostsRecord, RecordIdString } from '$lib/pocketbase-types';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function createHostsStore() {
|
||||
const hosts = writable<HostsRecord[]>([]);
|
||||
|
||||
async function fetchHosts() {
|
||||
const records = await pb.collection('hosts').getFullList();
|
||||
hosts.set(records);
|
||||
}
|
||||
|
||||
async function createHost(host: {
|
||||
ip: string;
|
||||
mac: string;
|
||||
name: string;
|
||||
port: number;
|
||||
user: RecordIdString;
|
||||
}) {
|
||||
await pb.collection('hosts').create(host);
|
||||
fetchHosts();
|
||||
}
|
||||
|
||||
async function updateHost(host: HostsRecord) {
|
||||
await pb.collection('hosts').update(host.id, host);
|
||||
fetchHosts();
|
||||
}
|
||||
|
||||
async function deleteHost(host: HostsRecord) {
|
||||
await pb.collection('hosts').delete(host.id);
|
||||
fetchHosts();
|
||||
}
|
||||
|
||||
async function wakeHost(host: HostsRecord) {
|
||||
pb.send('/api/wake', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ id: host.id })
|
||||
})
|
||||
.then((res) => {
|
||||
toast.success('WakeOnLan Magic Packet Sent');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to wake host', {
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...hosts,
|
||||
fetchHosts,
|
||||
createHost,
|
||||
updateHost,
|
||||
deleteHost,
|
||||
wakeHost
|
||||
};
|
||||
}
|
||||
|
||||
export const hostsStore = createHostsStore();
|
||||
6
apps/web/src/lib/utils.ts
Normal file
6
apps/web/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user