Lots of dev
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { User } from '$lib/types';
|
||||
import type { User } from '$lib/types/user';
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
@@ -49,22 +49,22 @@ export function setJWT(cookies: Cookies, user: User) {
|
||||
|
||||
export async function login(email: string, password: string): Promise<User> {
|
||||
try {
|
||||
const [user] = await sql`
|
||||
SELECT id, email, password_hash, perms, name
|
||||
const [user]: User[] = await sql`
|
||||
SELECT * FROM users
|
||||
WHERE email = ${email};
|
||||
`;
|
||||
|
||||
if (await bcrypt.compare(password, user.password_hash)) {
|
||||
if (await bcrypt.compare(password, user.password_hash!)) {
|
||||
delete user.password_hash;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
sql`
|
||||
UPDATE users
|
||||
SET last_signin = NOW()
|
||||
SET last_sign_in = NOW()
|
||||
WHERE id = ${user.id};
|
||||
`;
|
||||
|
||||
return <User>user;
|
||||
return user;
|
||||
}
|
||||
} catch {
|
||||
throw Error('Error signing in ');
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import ImagePlus from '@lucide/svelte/icons/image-plus';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
export let name = 'image';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let onSelect: ((file: File) => void) | null = null;
|
||||
|
||||
|
||||
let inputEl: HTMLInputElement | null = null;
|
||||
let previewUrl: string | null = null;
|
||||
let dragging = false;
|
||||
|
||||
function openFileDialog() {
|
||||
if (!disabled) {
|
||||
inputEl?.click();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiles(files: FileList | null) {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const selected = files[0];
|
||||
if (!selected.type.startsWith('image/')) return;
|
||||
|
||||
cleanupPreview();
|
||||
previewUrl = URL.createObjectURL(selected);
|
||||
|
||||
// Trigger callback
|
||||
if (onSelect) onSelect(selected);
|
||||
}
|
||||
|
||||
function onInputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
handleFiles(target.files);
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent) {
|
||||
e.preventDefault();
|
||||
dragging = false;
|
||||
if (disabled) return;
|
||||
handleFiles(e.dataTransfer?.files ?? null);
|
||||
}
|
||||
|
||||
function cleanupPreview() {
|
||||
if (previewUrl) {
|
||||
URL.revokeObjectURL(previewUrl);
|
||||
previewUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(cleanupPreview);
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="file"
|
||||
name={name}
|
||||
accept="image/*"
|
||||
required={required}
|
||||
disabled={disabled}
|
||||
capture="environment"
|
||||
on:change={onInputChange}
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
class="dropzone"
|
||||
class:has-image={!!previewUrl}
|
||||
class:dragging={dragging}
|
||||
on:click={openFileDialog}
|
||||
on:dragover|preventDefault={() => !disabled && (dragging = true)}
|
||||
on:dragleave={() => (dragging = false)}
|
||||
on:drop={onDrop}
|
||||
type="button"
|
||||
>
|
||||
{#if previewUrl}
|
||||
<img src={previewUrl} alt="Selected preview" />
|
||||
|
||||
<div class="overlay">
|
||||
<ImagePlus size={24} />
|
||||
<span>Replace image</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="placeholder py-4">
|
||||
<ImagePlus size={32} />
|
||||
<p>Click or drag an image here <span class="text-error">{required ? '*' : ''}</span></p>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if previewUrl}
|
||||
<button class="hover:text-destructive p-2" on:click={cleanupPreview} type="button">
|
||||
<X size={24} class="inline" />
|
||||
<span class="inline align-middle">Remove image</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropzone {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
min-height: 80px;
|
||||
/*height: 200px;*/
|
||||
border-radius: 12px;
|
||||
border: 2px dashed var(--border);
|
||||
/*background: var(--bg, #fafafa);*/
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.dropzone.dragging {
|
||||
border-color: var(--primary);
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.has-image:hover .overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.has-image:hover img {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
|
||||
import type { Item } from '$lib/types/item';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
|
||||
export let item: Item = <Item>{
|
||||
id: 2,
|
||||
// title: 'Water Bottle',
|
||||
foundDate: new Date(),
|
||||
approvedDate: new Date(),
|
||||
description: 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side.',
|
||||
transferred: true,
|
||||
foundLocation: 'By the tennis courts.'
|
||||
};
|
||||
export let admin = false;
|
||||
</script>
|
||||
|
||||
<a href="items/{item.id}"
|
||||
class="bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs">
|
||||
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" alt="" class="object-cover max-h-48">
|
||||
<div class="px-2 pb-2">
|
||||
<div>
|
||||
|
||||
<!-- <div class="font-bold inline-block">{item.title}</div>-->
|
||||
<!-- <div class="inline-block">-->
|
||||
{#if item.transferred}
|
||||
<Badge variant="secondary" class="float-right">In Lost & Found</Badge>
|
||||
{:else}
|
||||
<Badge variant="outline" class="float-right">With Finder</Badge>
|
||||
{/if}
|
||||
<div>{item.description}</div>
|
||||
{#if item.foundLocation}
|
||||
<div class="pt-2">
|
||||
<LocationIcon class="float-left mr-1" size={24} />
|
||||
<div>{item.foundLocation}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
FieldGroup,
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription
|
||||
} from '$lib/components/ui/field/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLFormAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLFormAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<form class={cn("flex flex-col gap-6", className)} {...restProps}>
|
||||
<FieldGroup>
|
||||
<div class="flex flex-col items-center gap-1 text-center">
|
||||
<h1 class="text-2xl font-bold">Login to your account</h1>
|
||||
<p class="text-muted-foreground text-sm text-balance">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel for="email">Email</FieldLabel>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<div class="flex items-center">
|
||||
<FieldLabel for="password">Password</FieldLabel>
|
||||
<a href="##" class="ms-auto text-sm underline-offset-4 hover:underline">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit">Login</Button>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldDescription class="text-center">
|
||||
Don't have an account?
|
||||
<a href="/signup" class="underline underline-offset-4">Sign up</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
@@ -1,49 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Field from '$lib/components/ui/field/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let { class: className, ...restProps }: HTMLAttributes<HTMLFormElement> = $props();
|
||||
</script>
|
||||
|
||||
<form class={cn("flex flex-col gap-6", className)} {...restProps}>
|
||||
<Field.Group>
|
||||
<div class="flex flex-col items-center gap-1 text-center">
|
||||
<h1 class="text-2xl font-bold">Create your account</h1>
|
||||
<p class="text-muted-foreground text-sm text-balance">
|
||||
Fill in the form below to create your account
|
||||
</p>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label for="name">Full Name</Field.Label>
|
||||
<Input id="name" type="text" placeholder="John Doe" required />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="email">Email</Field.Label>
|
||||
<Input id="email" type="email" placeholder="m@example.com" required />
|
||||
<Field.Description>
|
||||
We'll use this to contact you. We will not share your email with anyone else.
|
||||
</Field.Description>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="password">Password</Field.Label>
|
||||
<Input id="password" type="password" required />
|
||||
<Field.Description>Must be at least 8 characters long.</Field.Description>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="confirm-password">Confirm Password</Field.Label>
|
||||
<Input id="confirm-password" type="password" required />
|
||||
<Field.Description>Please confirm your password.</Field.Description>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Button type="submit">Create Account</Button>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Description class="px-6 text-center">
|
||||
Already have an account? <a href="/login">Sign in</a>
|
||||
</Field.Description>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
</form>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const badgeVariants = tv({
|
||||
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
href,
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||
variant?: BadgeVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
data-slot="badge"
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</svelte:element>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
indeterminate = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<CheckboxPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="checkbox"
|
||||
class={cn(
|
||||
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked, indeterminate })}
|
||||
<div data-slot="checkbox-indicator" class="text-current transition-none">
|
||||
{#if checked}
|
||||
<CheckIcon class="size-3.5" />
|
||||
{:else if indeterminate}
|
||||
<MinusIcon class="size-3.5" />
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CheckboxPrimitive.Root>
|
||||
@@ -0,0 +1,6 @@
|
||||
import Root from "./checkbox.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import DialogPortal from './dialog-portal.svelte';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Dialog from './index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
portalProps,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
|
||||
children: Snippet;
|
||||
showCloseButton?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<DialogPortal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="dialog-content"
|
||||
class={cn(
|
||||
"max-h-[calc(100%-2rem)] overflow-y-auto bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if showCloseButton}
|
||||
<DialogPrimitive.Close
|
||||
class="ring-offset-background focus:ring-ring absolute end-4 top-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
{/if}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="dialog-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-footer"
|
||||
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="dialog-header"
|
||||
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="dialog-overlay"
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="dialog-title"
|
||||
class={cn("text-lg leading-none font-semibold", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,34 @@
|
||||
import Root from "./dialog.svelte";
|
||||
import Portal from "./dialog-portal.svelte";
|
||||
import Title from "./dialog-title.svelte";
|
||||
import Footer from "./dialog-footer.svelte";
|
||||
import Header from "./dialog-header.svelte";
|
||||
import Overlay from "./dialog-overlay.svelte";
|
||||
import Content from "./dialog-content.svelte";
|
||||
import Description from "./dialog-description.svelte";
|
||||
import Trigger from "./dialog-trigger.svelte";
|
||||
import Close from "./dialog-close.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Close,
|
||||
//
|
||||
Root as Dialog,
|
||||
Title as DialogTitle,
|
||||
Portal as DialogPortal,
|
||||
Footer as DialogFooter,
|
||||
Header as DialogHeader,
|
||||
Trigger as DialogTrigger,
|
||||
Overlay as DialogOverlay,
|
||||
Content as DialogContent,
|
||||
Description as DialogDescription,
|
||||
Close as DialogClose,
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import Root from "./radio-group.svelte";
|
||||
import Item from "./radio-group-item.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as RadioGroup,
|
||||
Item as RadioGroupItem,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import CircleIcon from "@lucide/svelte/icons/circle";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="radio-group-item"
|
||||
class={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ checked })}
|
||||
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
|
||||
{#if checked}
|
||||
<CircleIcon
|
||||
class="fill-primary absolute start-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</RadioGroupPrimitive.Item>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: RadioGroupPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<RadioGroupPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="radio-group"
|
||||
class={cn("grid gap-3", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
import Root from "./select.svelte";
|
||||
import Group from "./select-group.svelte";
|
||||
import Label from "./select-label.svelte";
|
||||
import Item from "./select-item.svelte";
|
||||
import Content from "./select-content.svelte";
|
||||
import Trigger from "./select-trigger.svelte";
|
||||
import Separator from "./select-separator.svelte";
|
||||
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import GroupHeading from "./select-group-heading.svelte";
|
||||
import Portal from "./select-portal.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Label,
|
||||
Item,
|
||||
Content,
|
||||
Trigger,
|
||||
Separator,
|
||||
ScrollDownButton,
|
||||
ScrollUpButton,
|
||||
GroupHeading,
|
||||
Portal,
|
||||
//
|
||||
Root as Select,
|
||||
Group as SelectGroup,
|
||||
Label as SelectLabel,
|
||||
Item as SelectItem,
|
||||
Content as SelectContent,
|
||||
Trigger as SelectTrigger,
|
||||
Separator as SelectSeparator,
|
||||
ScrollDownButton as SelectScrollDownButton,
|
||||
ScrollUpButton as SelectScrollUpButton,
|
||||
GroupHeading as SelectGroupHeading,
|
||||
Portal as SelectPortal,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import SelectPortal from "./select-portal.svelte";
|
||||
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
children,
|
||||
preventScroll = true,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPortal {...portalProps}>
|
||||
<SelectPrimitive.Content
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
{preventScroll}
|
||||
data-slot="select-content"
|
||||
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-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
class={cn(
|
||||
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
|
||||
)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPortal>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="select-group-heading"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</SelectPrimitive.GroupHeading>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
label,
|
||||
children: childrenProp,
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Item
|
||||
bind:ref
|
||||
{value}
|
||||
data-slot="select-item"
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ selected, highlighted })}
|
||||
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||
{#if selected}
|
||||
<CheckIcon class="size-4" />
|
||||
{/if}
|
||||
</span>
|
||||
{#if childrenProp}
|
||||
{@render childrenProp({ selected, highlighted })}
|
||||
{:else}
|
||||
{label || value}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SelectPrimitive.Item>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="select-label"
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-down-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
bind:ref
|
||||
data-slot="select-scroll-up-button"
|
||||
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronUpIcon class="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<Separator
|
||||
bind:ref
|
||||
data-slot="select-separator"
|
||||
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronDownIcon class="size-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
value = $bindable(),
|
||||
...restProps
|
||||
}: SelectPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
"data-slot": dataSlot = "textarea",
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot={dataSlot}
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
></textarea>
|
||||
@@ -7,3 +7,22 @@ export const PERMISSIONS = {
|
||||
};
|
||||
|
||||
export const EXPIRE_REMINDER_DAYS = 30;
|
||||
|
||||
// const EMAIL_REGEX = new RegExp(
|
||||
// // eslint-disable-next-line no-control-regex
|
||||
// "([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\"\(\[\]!#-[^-~ \t]|(\\[\t -~]))+\")@([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\[[\t -Z^-~]*])"
|
||||
// );
|
||||
|
||||
// const EMAIL_REGEX = new RegExp(
|
||||
// /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
// );
|
||||
|
||||
// const EMAIL_REGEX =
|
||||
// // eslint-disable-next-line no-control-regex
|
||||
// /(?:[a-z0-9!#$%&'*+\/=?^`\{-\}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`\{-\}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
|
||||
|
||||
const EMAIL_REGEX =
|
||||
/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-.]*)[a-z0-9_'+-]@([a-z0-9][a-z0-9-]*\.)+[a-z]{2,}$/i;
|
||||
|
||||
// Replace single quote with HTML entity or remove it from the character class
|
||||
export const EMAIL_REGEX_STRING = EMAIL_REGEX.source.replace(/'/g, ''');
|
||||
|
||||
@@ -8,7 +8,8 @@ const sql = postgres({
|
||||
port: parseInt(process.env.POSTGRES_PORT!),
|
||||
database: process.env.POSTGRES_DB,
|
||||
username: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
transform: postgres.camel
|
||||
});
|
||||
|
||||
export default sql;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import type { User } from '$lib/types';
|
||||
import sql from '$lib/db/db.server';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
// should require MANAGE_USERS permission
|
||||
|
||||
export async function getUsers(searchQuery: string | null = null): Promise<User[]> {
|
||||
return sql`
|
||||
SELECT id,
|
||||
email,
|
||||
perms,
|
||||
name,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
||||
FROM users
|
||||
WHERE (${!searchQuery ? sql`TRUE` : sql`email ILIKE ${'%' + searchQuery + '%'} OR name ILIKE ${'%' + searchQuery + '%'}`});
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getUser(id: number): Promise<User> {
|
||||
return <User>(
|
||||
await sql`
|
||||
SELECT id,
|
||||
email,
|
||||
perms,
|
||||
name,
|
||||
created_at AT TIME ZONE 'UTC' AS "createdAt",
|
||||
last_signin AT TIME ZONE 'UTC' AS "lastSignIn"
|
||||
FROM users
|
||||
WHERE (id = ${id}) LIMIT 1;
|
||||
`
|
||||
)[0];
|
||||
}
|
||||
|
||||
export async function createUser(user: User) {
|
||||
const passwordHash = await bcrypt.hash(user.password!, 12);
|
||||
await sql`
|
||||
INSERT INTO users (email, password_hash, perms, name, created_at, last_signin)
|
||||
VALUES (${user.email}, ${passwordHash}, ${user.perms}, ${user.name}, NOW(), NOW())
|
||||
RETURNING id;
|
||||
`;
|
||||
}
|
||||
|
||||
export async function editUser(user: User) {
|
||||
let passwordHash: string | undefined;
|
||||
if (user.password) {
|
||||
passwordHash = await bcrypt.hash(user.password, 12);
|
||||
}
|
||||
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET
|
||||
email = ${user.email},
|
||||
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
|
||||
perms = ${user.perms},
|
||||
name = ${user.name}
|
||||
WHERE id = ${user.id!}
|
||||
RETURNING id;
|
||||
`;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number) {
|
||||
await sql`
|
||||
DELETE FROM users
|
||||
WHERE id = ${id}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// Create a transporter object using SMTP transport
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: Number(process.env.EMAIL_PORT),
|
||||
secure: true, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
export async function sendEmployerNotificationEmail() {
|
||||
// Send mail with defined transport object
|
||||
await transporter.sendMail({
|
||||
from: `CareerConnect Notifications <${process.env.EMAIL_USER}>`,
|
||||
// to: info.emails.join(', '), // EMAILING OF REAL COMPANIES DISABLED, UNCOMMENT TO ENABLE
|
||||
to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
|
||||
subject: 'New Application Received!',
|
||||
text: `A new application has been received for the posting ''!\n\nCheck it out at ${process.env.BASE_URL}/`
|
||||
});
|
||||
}
|
||||
+11
-4
@@ -1,4 +1,5 @@
|
||||
import type { User } from '$lib/types';
|
||||
import type { User } from '$lib/types/user';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
|
||||
export const getCookieValue = (name: string): string =>
|
||||
document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)')?.pop() || '';
|
||||
@@ -21,15 +22,21 @@ export function getFormString(data: FormData, key: string): string | undefined {
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw Error(`Incorrect input in field ${key}.`);
|
||||
throw fail(400, {
|
||||
error: `Incorrect input in field ${key}.`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function getRequiredFormString(data: FormData, key: string) {
|
||||
const value = data.get(key);
|
||||
if (typeof value !== 'string') {
|
||||
throw Error(`Missing required field ${key}.`);
|
||||
if (typeof value !== 'string' || value === '') {
|
||||
throw fail(400, {
|
||||
error: `Missing required field ${key}.`,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
phone: string;
|
||||
password?: string;
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
lastSignIn: Date;
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
id: number;
|
||||
ownerEmail: string;
|
||||
ownerPhone: string;
|
||||
foundDate: Date;
|
||||
approvedDate?: Date;
|
||||
claimedDate?: Date;
|
||||
title: string;
|
||||
description: string;
|
||||
transferred: boolean; // to L&F location
|
||||
keywords?: string[];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum Sender {
|
||||
ADMIN = 'admin',
|
||||
FINDER = 'finder',
|
||||
INQUIRER = 'inquirer'
|
||||
}
|
||||
|
||||
export interface message {
|
||||
id: number;
|
||||
threadId: number;
|
||||
sender: Sender;
|
||||
body: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
export interface Item {
|
||||
id: number;
|
||||
emails?: string[];
|
||||
// ownerPhone: string;
|
||||
foundDate: Date;
|
||||
approvedDate?: Date;
|
||||
claimedDate?: Date;
|
||||
// title: string;
|
||||
description: string;
|
||||
transferred: boolean; // to L&F location
|
||||
keywords?: string[];
|
||||
foundLocation: string;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
name: string;
|
||||
password?: string;
|
||||
password_hash?: string;
|
||||
settings?: UserSettings;
|
||||
createdAt: Date;
|
||||
lastSignIn: Date;
|
||||
}
|
||||
|
||||
export interface UserPayload {
|
||||
id: number;
|
||||
email: string;
|
||||
username: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
staleItemDays: number;
|
||||
notifyAllApprovedInquiries?: boolean;
|
||||
notifyAllTurnedInInquiries?: boolean;
|
||||
}
|
||||
|
||||
export const DefaultUserSettings: UserSettings = {
|
||||
staleItemDays: 30,
|
||||
notifyAllApprovedInquiries: false,
|
||||
notifyAllTurnedInInquiries: false
|
||||
};
|
||||
Reference in New Issue
Block a user