first commit

This commit is contained in:
Leandro Afonso
2025-10-19 20:47:58 +01:00
commit 63f850d705
80 changed files with 8358 additions and 0 deletions

View 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>

View 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>

View 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>

View 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}

View 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,
};

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View 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} />

View 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}
/>

View File

@@ -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>

View 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>

View 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>

View 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} />

View 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>

View 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}
/>

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,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View 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}
/>

View File

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

View 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}
/>

View 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,
};

View File

@@ -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>

View File

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

View 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}
/>

View File

@@ -0,0 +1 @@
export { default as Toaster } from "./sonner.svelte";

View 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}
/>

View 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
View 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;

View 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>
}

View 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();

View 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));
}