First commit

This commit is contained in:
Bradley Shellnut 2024-12-26 10:49:41 -08:00
commit 12d8384fa4
181 changed files with 13966 additions and 0 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DATABASE_URL="postgres://root:mysecretpassword@localhost:5432/local"

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
test-results
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

15
.storybook/main.js Normal file
View file

@ -0,0 +1,15 @@
/** @type { import('@storybook/sveltekit').StorybookConfig } */
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|ts|svelte)"],
addons: [
"@storybook/addon-svelte-csf",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/sveltekit",
options: {},
},
};
export default config;

13
.storybook/preview.js Normal file
View file

@ -0,0 +1,13 @@
/** @type { import('@storybook/svelte').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

3
Dockerfile.minio Normal file
View file

@ -0,0 +1,3 @@
FROM nginx:stable-alpine-slim
COPY minio-console.conf.template /etc/nginx/templates/
RUN rm /etc/nginx/conf.d/default.conf /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh

38
README.md Normal file
View file

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

65
biome.json Normal file
View file

@ -0,0 +1,65 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": { "ignoreUnknown": false, "ignore": [] },
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 150,
"attributePosition": "auto",
"ignore": [
"**/.DS_Store",
"**/node_modules",
"./build",
"./.svelte-kit",
"./package",
"**/.env",
"**/.env.*",
"**/pnpm-lock.yaml",
"**/package-lock.json",
"**/yarn.lock",
"**/paraglide/**"
]
},
"organizeImports": { "enabled": true },
"linter": { "enabled": true, "rules": { "recommended": true } },
"javascript": {
"formatter": {
"jsxQuoteStyle": "single",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"indentStyle": "space",
"lineEnding": "lf",
"lineWidth": 150,
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto"
},
"parser": {
"unsafeParameterDecoratorsEnabled": true
}
},
"overrides": [
{
"include": ["*.svelte"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useImportType": "off"
}
}
}
}
]
}

17
components.json Normal file
View file

@ -0,0 +1,17 @@
{
"$schema": "https://next.shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}

63
docker-compose.yml Normal file
View file

@ -0,0 +1,63 @@
version: "3.8"
services:
postgres:
image: postgres:latest
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/data
networks:
- app-network
redis:
image: redis:latest
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app-network
minio:
image: docker.io/bitnami/minio
ports:
- "9000:9000"
- "9001:9001"
networks:
- app-network
volumes:
- "minio_data:/data"
environment:
- MINIO_ROOT_USER=user
- MINIO_ROOT_PASSWORD=password
- MINIO_DEFAULT_BUCKETS=dev
# mailpit:
# image: axllent/mailpit
# volumes:
# - mailpit_data:/data
# ports:
# - 8025:8025
# - 1025:1025
# environment:
# MP_MAX_MESSAGES: 5000
# MP_DATABASE: /data/mailpit.db
# MP_SMTP_AUTH_ACCEPT_ANY: 1
# MP_SMTP_AUTH_ALLOW_INSECURE: 1
# networks:
# - app-network
volumes:
postgres_data:
redis_data:
# mailpit_data:
minio_data:
driver: local
networks:
app-network:
driver: bridge

23
drizzle.config.ts Normal file
View file

@ -0,0 +1,23 @@
import { defineConfig } from 'drizzle-kit';
/* ------------------------------- !IMPORTANT ------------------------------- */
/* ---------------- Before running migrations or generations ---------------- */
/* ------------------ make sure to build the project first ------------------ */
/* -------------------------------------------------------------------------- */
export default defineConfig({
out: './drizzle',
schema: './src/lib/server/api/databases/postgres/drizzle-schema.ts',
breakpoints: false,
strict: true,
verbose: true,
dialect: 'postgresql',
casing: 'snake_case',
dbCredentials: {
url: process.env.DATABASE_URL!
},
migrations: {
table: 'migrations',
schema: 'public'
}
});

6
e2e/demo.test.ts Normal file
View file

@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

4
messages/en.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!"
}

4
messages/es.json Normal file
View file

@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from es!"
}

105
package.json Normal file
View file

@ -0,0 +1,105 @@
{
"name": "musicle-svelte",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"test": "npm run test:unit -- --run && npm run test:e2e",
"test:e2e": "playwright test",
"db:start": "docker compose up",
"db:push": "drizzle-kit push",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@chromatic-com/storybook": "^3.2.3",
"@faker-js/faker": "^9.3.0",
"@playwright/test": "^1.45.3",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-svelte-csf": "^5.0.0-next.21",
"@storybook/blocks": "^8.4.7",
"@storybook/svelte": "^8.4.7",
"@storybook/sveltekit": "^8.4.7",
"@storybook/test": "^8.4.7",
"@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/cookie": "^1.0.0",
"@types/node": "^22.10.2",
"@types/pg": "^8.11.10",
"@types/qrcode": "^1.5.5",
"autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.74",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.1",
"formsnap": "^2.0.0",
"lucide-svelte": "^0.469.0",
"storybook": "^8.4.7",
"svelte": "^5.16.0",
"svelte-check": "^4.0.0",
"svelte-headless-table": "^0.18.3",
"svelte-meta-tags": "^4.0.4",
"svelte-preprocess": "^6.0.3",
"svelte-sequential-preprocessor": "^2.0.2",
"svelte-sonner": "^0.3.28",
"sveltekit-flash-message": "^2.4.4",
"sveltekit-superforms": "^2.22.1",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.9",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.0.0",
"vite": "^6.0.6",
"vitest": "^2.0.4",
"zod": "^3.24.1"
},
"dependencies": {
"@hono/swagger-ui": "^0.5.0",
"@hono/zod-openapi": "^0.18.3",
"@hono/zod-validator": "^0.4.2",
"@inlang/paraglide-sveltekit": "^0.15.0",
"@needle-di/core": "^0.8.4",
"@oslojs/binary": "^1.0.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@oslojs/jwt": "^0.3.0",
"@oslojs/oauth2": "^0.5.0",
"@oslojs/otp": "^1.0.0",
"@oslojs/webauthn": "^1.0.0",
"@scalar/hono-api-reference": "^0.5.162",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"arctic": "^2.3.3",
"argon2": "^0.41.1",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.38.3",
"drizzle-zod": "^0.6.1",
"hono": "^4.6.14",
"hono-pino": "^0.7.0",
"hono-rate-limiter": "^0.4.2",
"hono-zod-openapi": "^0.5.0",
"ioredis": "^5.4.2",
"minio": "^8.0.3",
"mode-watcher": "^0.5.0",
"nanoid": "^5.0.9",
"pg": "^8.13.1",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"postgres": "^3.4.4",
"rate-limit-redis": "^4.2.0",
"sharp": "^0.33.5",
"stoker": "^1.4.2"
}
}

10
playwright.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e'
});

8769
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

1
project.inlang/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
cache

View file

@ -0,0 +1,20 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-identical-pattern@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-valid-js-identifier@1/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@2/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@0/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{languageTag}.json"
},
"sourceLanguageTag": "en",
"languageTags": [
"en",
"es"
]
}

75
src/app.css Normal file
View file

@ -0,0 +1,75 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

13
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Normal file
View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="%paraglide.lang%" dir="%paraglide.textDirection%">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

8
src/hooks.server.ts Normal file
View file

@ -0,0 +1,8 @@
import type { Handle } from '@sveltejs/kit';
import { i18n } from '$lib/i18n';
import { sequence } from '@sveltejs/kit/hooks';
import { startServer } from '$lib/server/api';
const handleParaglide: Handle = i18n.handle();
startServer();
export const handle: Handle = sequence(handleParaglide);

2
src/hooks.ts Normal file
View file

@ -0,0 +1,2 @@
import { i18n } from '$lib/i18n';
export const reroute = i18n.reroute();

View file

@ -0,0 +1,25 @@
<script lang="ts">
import Moon from 'lucide-svelte/icons/moon';
import Sun from 'lucide-svelte/icons/sun';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { resetMode, setMode } from 'mode-watcher';
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger class={buttonVariants({ variant: 'outline', size: 'icon' })}>
<Sun
class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
<span class="sr-only">Toggle theme</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={() => setMode('light')}>Light</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => setMode('dark')}>Dark</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => resetMode()}>System</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

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,38 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
} = $props();
</script>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
class={cn(
"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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
{...restProps}
>
{@render children?.()}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</Dialog.Portal>

View file

@ -0,0 +1,16 @@
<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
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

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("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
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 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,19 @@
<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
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/80",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
<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
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
/>

View file

@ -0,0 +1,37 @@
import { Dialog as DialogPrimitive } from "bits-ui";
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";
const Root = DialogPrimitive.Root;
const Trigger = DialogPrimitive.Trigger;
const Close = DialogPrimitive.Close;
const Portal = DialogPrimitive.Portal;
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,
};

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if indeterminate}
<Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
{sideOffset}
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 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
/>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { type WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
import Circle from "lucide-svelte/icons/circle";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span class="absolute left-2 flex size-3.5 items-center justify-center">
{#if checked}
<Circle class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
class={cn(
"bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRight class="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,50 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
const Trigger = DropdownMenuPrimitive.Trigger;
const Group = DropdownMenuPrimitive.Group;
const RadioGroup = DropdownMenuPrimitive.RadioGroup;
export {
CheckboxItem,
Content,
Root as DropdownMenu,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
GroupHeading as DropdownMenuGroupHeading,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};

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,15 @@
import Root from "./input-otp.svelte";
import Group from "./input-otp-group.svelte";
import Slot from "./input-otp-slot.svelte";
import Separator from "./input-otp-separator.svelte";
export {
Root,
Group,
Slot,
Separator,
Root as InputOTP,
Group as InputOTPGroup,
Slot as InputOTPSlot,
Separator as InputOTPSeparator,
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
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", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import Dot from "lucide-svelte/icons/dot";
let {
ref = $bindable(null),
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} role="separator" {...restProps}>
{#if children}
{@render children?.()}
{:else}
<Dot />
{/if}
</div>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import { PinInput as InputOTPPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
cell,
class: className,
...restProps
}: ComponentProps<typeof InputOTPPrimitive.Cell> = $props();
</script>
<InputOTPPrimitive.Cell
{cell}
bind:ref
class={cn(
"border-input relative flex h-10 w-10 items-center justify-center border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
cell.isActive && "ring-ring ring-offset-background z-10 ring-2",
className
)}
{...restProps}
>
{cell.char}
{#if cell.hasFakeCaret}
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<div class="animate-caret-blink bg-foreground h-4 w-px duration-1000"></div>
</div>
{/if}
</InputOTPPrimitive.Cell>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { PinInput as InputOTPPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: ComponentProps<typeof InputOTPPrimitive.Root> = $props();
</script>
<InputOTPPrimitive.Root
bind:ref
bind:value
class={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50 [&_input]:disabled:cursor-not-allowed",
className
)}
{...restProps}
/>

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,36 @@
import { Dialog as SheetPrimitive } from "bits-ui";
import Overlay from "./sheet-overlay.svelte";
import Content from "./sheet-content.svelte";
import Header from "./sheet-header.svelte";
import Footer from "./sheet-footer.svelte";
import Title from "./sheet-title.svelte";
import Description from "./sheet-description.svelte";
const Root = SheetPrimitive.Root;
const Close = SheetPrimitive.Close;
const Trigger = SheetPrimitive.Trigger;
const Portal = SheetPrimitive.Portal;
export {
Root,
Close,
Trigger,
Portal,
Overlay,
Content,
Header,
Footer,
Title,
Description,
//
Root as Sheet,
Close as SheetClose,
Trigger as SheetTrigger,
Portal as SheetPortal,
Overlay as SheetOverlay,
Content as SheetContent,
Header as SheetHeader,
Footer as SheetFooter,
Title as SheetTitle,
Description as SheetDescription,
};

View file

@ -0,0 +1,53 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const sheetVariants = tv({
base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
variants: {
side: {
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
});
export type Side = VariantProps<typeof sheetVariants>["side"];
</script>
<script lang="ts">
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import type { Snippet } from "svelte";
import SheetOverlay from "./sheet-overlay.svelte";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
side = "right",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: SheetPrimitive.PortalProps;
side?: Side;
children: Snippet;
} = $props();
</script>
<SheetPrimitive.Portal {...portalProps}>
<SheetOverlay />
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
{@render children?.()}
<SheetPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
>
<X class="size-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPrimitive.Portal>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.DescriptionProps = $props();
</script>
<SheetPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

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("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
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-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.OverlayProps = $props();
export { className as class };
</script>
<SheetPrimitive.Overlay
bind:ref
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/80",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.TitleProps = $props();
</script>
<SheetPrimitive.Title
bind:ref
class={cn("text-foreground text-lg font-semibold", className)}
{...restProps}
/>

View file

@ -0,0 +1,27 @@
import { untrack } from "svelte";
const MOBILE_BREAKPOINT = 768;
export class IsMobile {
#current = $state<boolean>(false);
constructor() {
$effect(() => {
return untrack(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
this.#current = window.innerWidth < MOBILE_BREAKPOINT;
};
mql.addEventListener("change", onChange);
onChange();
return () => {
mql.removeEventListener("change", onChange);
};
});
});
}
get current() {
return this.#current;
}
}

View file

@ -0,0 +1,20 @@
import type { Api } from '$lib/utils/types';
import type { InferResponseType } from 'hono';
type Me = InferResponseType<Api['users']['me']['$get']>;
class AuthContext {
#authedUser = $state<Me>(null);
isAuthed = $derived(!!this.#authedUser);
get authedUser() {
return this.#authedUser;
}
// I like to be explicit when setting the authed user
setAuthedUser(user: Me) {
this.#authedUser = user;
}
}
export const authContext = new AuthContext();

3
src/lib/i18n.ts Normal file
View file

@ -0,0 +1,3 @@
import * as runtime from '$lib/paraglide/runtime';
import { createI18n } from '@inlang/paraglide-sveltekit';
export const i18n = createI18n(runtime);

1
src/lib/index.ts Normal file
View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,56 @@
import { inject, injectable } from '@needle-di/core';
import { contextStorage } from 'hono/context-storage';
import { notFound, onError, serveEmojiFavicon } from 'stoker/middlewares';
import { sessionManagement } from './common/middleware/session-managment.middleware';
import { rateLimit } from './common/middleware/rate-limit.middleware';
import { RootController } from './common/factories/controllers.factory';
import { requestId } from 'hono/request-id';
import { generateId } from './common/utils/crypto';
import { UsersController } from './users/users.controller';
import { browserSessions } from './common/middleware/browser-session.middleware';
import { IamController } from './iam/iam.controller';
import configureOpenAPI from './configure-open-api';
import { pinoLogger } from './common/middleware/pino-logger.middleware';
@injectable()
export class ApplicationController extends RootController {
constructor(
private iamController = inject(IamController),
private usersController = inject(UsersController)
) {
super();
}
routes() {
return this.controller
.get('/', (c) => {
return c.json({ status: 'ok' });
})
.get('/healthz', (c) => {
return c.json({ status: 'ok' });
})
.get('/rate-limit', rateLimit({ limit: 3, minutes: 1 }), (c) => {
return c.json({ message: 'Test!' });
});
}
registerControllers() {
const app = this.controller;
app.onError(onError);
app.notFound(notFound);
app
.basePath('/api')
.use(requestId({ generator: () => generateId() }))
.use(contextStorage())
.use(browserSessions)
.use(sessionManagement)
.use(serveEmojiFavicon('📝'))
.use(pinoLogger())
.route('/', this.routes())
.route('/iam', this.iamController.routes())
.route('/users', this.usersController.routes());
configureOpenAPI(app);
return app;
}
}

View file

@ -0,0 +1,42 @@
import { inject, injectable } from '@needle-di/core';
import { ConfigService } from './common/configs/config.service';
import { ApplicationController } from './application.controller';
import { StorageService } from './storage/storage.service';
@injectable()
export class ApplicationModule {
constructor(
private applicationController = inject(ApplicationController),
private configService = inject(ConfigService),
private storageService = inject(StorageService)
) {}
async app() {
return this.applicationController.registerControllers();
}
async start() {
const app = this.app();
await this.onApplicationStartup();
// register shutdown hooks
process.on('SIGINT', this.onApplicationShutdown);
process.on('SIGTERM', this.onApplicationShutdown);
console.log(`Api started on port ${this.configService.envs.PORT}`);
return app;
}
private async onApplicationStartup() {
console.log('Application startup...');
// validate configs
this.configService.validateEnvs();
// configure storage service
await this.storageService.configure();
}
private onApplicationShutdown() {
console.log('Shutting down...');
process.exit();
}
}

View file

@ -0,0 +1,31 @@
import { injectable } from '@needle-di/core';
import { z } from 'zod';
import { type EnvsDto, envsDto } from './dtos/env.dto';
import * as envs from '$env/static/private';
@injectable()
export class ConfigService {
envs: EnvsDto;
constructor() {
this.envs = this.parseEnvs()!;
}
private parseEnvs() {
return envsDto.parse(envs);
}
validateEnvs() {
try {
return envsDto.parse(envs);
} catch (err) {
if (err instanceof z.ZodError) {
const { fieldErrors } = err.flatten();
const errorMessage = Object.entries(fieldErrors)
.map(([field, errors]) => (errors ? `${field}: ${errors.join(', ')}` : field))
.join('\n ');
throw new Error(`Missing environment variables:\n ${errorMessage}`);
}
}
}
}

View file

@ -0,0 +1,31 @@
import { z } from 'zod';
const stringBoolean = z.coerce
.string()
.transform((val) => {
return val === 'true';
})
.default('false');
export const envsDto = z.object({
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.coerce.number(),
DATABASE_DB: z.string(),
DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean,
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
NODE_ENV: z.string().default('development'),
ORIGIN: z.string(),
REDIS_URL: z.string(),
SIGNING_SECRET: z.string(),
ENV: z.enum(['dev', 'prod']),
PORT: z.number({ coerce: true }),
STORAGE_HOST: z.string(),
STORAGE_PORT: z.number({ coerce: true }),
STORAGE_ACCESS_KEY: z.string(),
STORAGE_SECRET_KEY: z.string()
});
export type EnvsDto = z.infer<typeof envsDto>;

View file

@ -0,0 +1,13 @@
import { createHono } from '../utils/hono';
export abstract class Controller {
protected readonly controller: ReturnType<typeof createHono>;
constructor() {
this.controller = createHono();
}
abstract routes(): ReturnType<typeof createHono>;
}
export abstract class RootController extends Controller {
abstract registerControllers(): ReturnType<typeof createHono>;
}

View file

@ -0,0 +1,9 @@
import { Container } from '@needle-di/core';
import { DrizzleService } from '../../databases/postgres/drizzle.service';
export abstract class DrizzleRepository {
protected readonly drizzle: DrizzleService;
constructor() {
this.drizzle = new Container().get(DrizzleService);
}
}

View file

@ -0,0 +1,7 @@
import { Container } from '@needle-di/core';
import { RedisService } from '../../databases/redis/redis.service';
export abstract class RedisRepository<T extends string> {
protected readonly redis = new Container().get(RedisService);
readonly prefix: T | string = '';
}

View file

@ -0,0 +1,42 @@
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
import { Unauthorized } from '../utils/exceptions';
import type { SessionDto } from '../../iam/sessions/dtos/session.dto';
/* ---------------------------------- Types --------------------------------- */
type AuthStates = 'session' | 'none';
type AuthedReturnType = typeof authed;
type UnauthedReturnType = typeof unauthed;
/* ------------------- Overloaded function implementation ------------------- */
// we have to overload the implementation to provide the correct return type
export function authState(state: 'session'): AuthedReturnType;
export function authState(state: 'none'): UnauthedReturnType;
export function authState(state: AuthStates): AuthedReturnType | UnauthedReturnType {
if (state === 'session') return authed;
return unauthed;
}
/* ------------------------------ Require Auth ------------------------------ */
const authed: MiddlewareHandler<{
Variables: {
session: SessionDto;
};
}> = createMiddleware(async (c, next) => {
if (!c.var.session) {
throw Unauthorized('You must be logged in to access this resource');
}
return next();
});
/* ---------------------------- Require Unauthed ---------------------------- */
const unauthed: MiddlewareHandler<{
Variables: {
session: null;
};
}> = createMiddleware(async (c, next) => {
if (c.var.session) {
throw Unauthorized('You must be logged out to access this resource');
}
return next();
});

View file

@ -0,0 +1,37 @@
import type { MiddlewareHandler } from 'hono';
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
import { createMiddleware } from 'hono/factory';
import { generateId } from '../utils/crypto';
import { Container } from '@needle-di/core';
import { ConfigService } from '../configs/config.service';
export const browserSessions: MiddlewareHandler = createMiddleware(async (c, next) => {
const BROWSER_SESSION_COOKIE_NAME = '_id';
const container = new Container();
const configService = container.get(ConfigService);
const browserSessionCookie = await getSignedCookie(
c,
configService.envs.SIGNING_SECRET,
BROWSER_SESSION_COOKIE_NAME
);
let browserSessionId = browserSessionCookie;
if (!browserSessionCookie) {
browserSessionId = generateId();
setSignedCookie(
c,
BROWSER_SESSION_COOKIE_NAME,
browserSessionId,
configService.envs.SIGNING_SECRET,
{
httpOnly: true,
sameSite: 'lax',
secure: configService.envs.ENV === 'prod',
path: '/'
}
);
}
c.set('browserSessionId', browserSessionId);
return next();
});

View file

@ -0,0 +1,18 @@
import { pinoLogger as logger } from "hono-pino";
import { Container } from '@needle-di/core';
import { ConfigService } from '../configs/config.service';
import pino from "pino";
import pretty from "pino-pretty";
export function pinoLogger() {
const container = new Container();
const configService = container.get(ConfigService);
return logger({
pino: pino(
{
level: configService.envs.LOG_LEVEL || "info",
},
configService.envs.NODE_ENV === "production" ? undefined : pretty(),
),
});
}

View file

@ -0,0 +1,35 @@
import { rateLimiter } from 'hono-rate-limiter';
import { RedisStore } from 'rate-limit-redis';
import { Container } from '@needle-di/core';
import { RedisService } from '../../databases/redis/redis.service';
import type { Context } from 'hono';
import type { HonoEnv } from '../utils/hono';
const container = new Container();
const client = container.get(RedisService).redis;
export function rateLimit({
limit,
minutes,
key = ''
}: {
limit: number;
minutes: number;
key?: string;
}) {
return rateLimiter({
windowMs: minutes * 60 * 1000, // every x minutes
limit, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: 'draft-6', // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
keyGenerator: (c: Context<HonoEnv>) => {
const clientKey = c.var.session?.userId || c.req.header('x-forwarded-for');
const pathKey = key || c.req.routePath;
return `${clientKey}_${pathKey}`;
}, // Method to generate custom identifiers for clients.
// Redis store configuration
store: new RedisStore({
// @ts-expect-error - Known issue: the `call` function is not present in @types/ioredis
sendCommand: (...args: string[]) => client.call(...args)
}) as never
});
}

View file

@ -0,0 +1,30 @@
import type { MiddlewareHandler } from 'hono';
import { createMiddleware } from 'hono/factory';
import { Container } from '@needle-di/core';
import { SessionsService } from '../../iam/sessions/sessions.service';
export const sessionManagement: MiddlewareHandler = createMiddleware(async (c, next) => {
const container = new Container();
const sessionService = container.get(SessionsService);
const sessionId = await sessionService.getSessionCookie();
if (!sessionId) {
c.set('session', null);
return next();
}
// Validate the session
const session = await sessionService.validateSession(sessionId);
// If the session is not found, delete the cookie
if (!session) sessionService.deleteSessionCookie();
// If the session is fresh, refresh the cookie with the new expiration date
if (session && session.fresh) sessionService.setSessionCookie(session);
// Set the session in the context
c.set('session', session);
// Continue to the next middleware
return next();
});

View file

@ -0,0 +1,30 @@
import { Container } from '@needle-di/core';
import { describe, expect, it } from 'vitest';
import { HashingService } from './hashing.service';
describe('Hashing Service', () => {
const container = new Container();
const service = container.get(HashingService);
it('should hash a value', async () => {
const value = 'password';
const hashedValue = await service.hash('password');
expect(hashedValue).not.toEqual(value);
});
it('should validate a correctly hashed value', async () => {
const value = 'password';
const hashedValue = await service.hash('password');
const comparitor = await service.compare(value, hashedValue);
expect(comparitor).toEqual(true);
});
it('should invalidate an incorrectly hashed value', async () => {
const value = 'password';
const badHash = await service.hash('notPassword');
const comparitor = await service.compare(value, badHash);
expect(comparitor).toEqual(false);
});
});

View file

@ -0,0 +1,13 @@
import { hash, verify } from 'argon2';
import { injectable } from '@needle-di/core';
@injectable()
export class HashingService {
hash(data: string): Promise<string> {
return hash(data);
}
compare(data: string, encrypted: string): Promise<boolean> {
return verify(encrypted, data);
}
}

View file

@ -0,0 +1,22 @@
import { getContext } from 'hono/context-storage';
import { injectable } from '@needle-di/core';
import type { HonoEnv } from '../utils/hono';
@injectable()
export class RequestContextService {
getContext() {
return getContext<HonoEnv>();
}
getRequestId(): string {
return this.getContext().get('requestId');
}
getAuthedUserId() {
return this.getContext().get('session')?.userId;
}
getSession() {
return this.getContext().get('session');
}
}

View file

@ -0,0 +1,30 @@
import { inject, injectable } from "@needle-di/core";
import { generateId } from "../../common/utils/crypto";
import { HashingService } from "../../common/services/hashing.service";
@injectable()
export class VerificationCodesService {
constructor(private hashingService = inject(HashingService)) {}
async generateCodeWithHash() {
const verificationCode = this.generateCode();
const hashedVerificationCode = await this.hashingService.hash(
verificationCode,
);
return { verificationCode, hashedVerificationCode };
}
verify(args: { verificationCode: string; hashedVerificationCode: string }) {
return this.hashingService.compare(
args.verificationCode,
args.hashedVerificationCode,
);
}
private generateCode() {
// alphabet with removed look-alike characters (0, 1, O, I)
const alphabet = "23456789ACDEFGHJKLMNPQRSTUVWXYZ";
// generate 6 character long random string
return generateId(6, alphabet);
}
}

View file

@ -0,0 +1,17 @@
import { customAlphabet } from 'nanoid';
// generateId is a function that returns a new unique identifier.
// ~4 million years or 30 trillion IDs needed, in order to have a 1% probability of at least one collision.
// So, TLDR; by the time a collision happens, you and your next 100 generations will be long gone,
// the lizard people will have taken over, the robots will have enslaved them, and the roomba uprising will be in full swing.
// All hail king roomba, the first of his name, the unclean, king of the dust bunnies and the first allergens, lord of the seven corners, and protector of the realm.
// https://zelark.github.io/nano-id-cc/
export function generateId(
length = 16,
alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
) {
const nanoId = customAlphabet(alphabet, length);
return nanoId();
}

View file

@ -0,0 +1,53 @@
import { customType, timestamp } from 'drizzle-orm/pg-core';
import { NotFound } from './exceptions';
/* -------------------------------------------------------------------------- */
/* Repository */
/* -------------------------------------------------------------------------- */
// get the first element of an array or return null
export const takeFirst = <T>(values: T[]): T | null => {
return values.shift() || null;
};
// get the first element of an array or throw a 404 error
export const takeFirstOrThrow = <T>(values: T[]): T => {
const value = values.shift();
if (!value) throw NotFound('The requested resource was not found.');
return value;
};
/* -------------------------------------------------------------------------- */
/* Tables */
/* -------------------------------------------------------------------------- */
// custom type for citext
export const citext = customType<{ data: string }>({
dataType() {
return 'citext';
}
});
// custom type for generating an id
export const id = customType<{ data: string }>({
dataType() {
return 'text';
}
});
// timestamps for created_at and updated_at
export const timestamps = {
createdAt: timestamp('created_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow(),
updatedAt: timestamp('updated_at', {
mode: 'date',
withTimezone: true
})
.notNull()
.defaultNow()
.$onUpdateFn(() => new Date())
};

View file

@ -0,0 +1,26 @@
import { HTTPException } from 'hono/http-exception';
import { StatusCodes } from '../../../../utils/status-codes';
export function TooManyRequests(message: string = 'Too many requests') {
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
}
export function Forbidden(message: string = 'Forbidden') {
return new HTTPException(StatusCodes.FORBIDDEN, { message });
}
export function Unauthorized(message: string = 'Unauthorized') {
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
}
export function NotFound(message: string = 'Not Found') {
return new HTTPException(StatusCodes.NOT_FOUND, { message });
}
export function BadRequest(message: string = 'Bad Request') {
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
}
export function InternalError(message: string = 'Internal Error') {
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
}

View file

@ -0,0 +1,14 @@
import { Hono } from 'hono';
import type { SessionDto } from '../../iam/sessions/dtos/session.dto';
export type HonoEnv = {
Variables: {
session: SessionDto | null;
browserSessionId: string;
requestId: string;
};
};
export function createHono() {
return new Hono<HonoEnv>();
}

View file

@ -0,0 +1,42 @@
import { apiReference } from '@scalar/hono-api-reference';
import { createOpenApiDocument } from 'hono-zod-openapi';
import packageJSON from '../../../../package.json';
import type { AppOpenAPI } from './common/utils/hono';
export default function configureOpenAPI(app: AppOpenAPI) {
createOpenApiDocument(app, {
info: {
title: 'Bored Game API',
description: 'Bored Game API',
version: packageJSON.version,
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
cookieAuth: {
type: 'apiKey',
name: 'session',
in: 'cookie',
},
},
},
});
app.get(
'/reference',
apiReference({
theme: 'kepler',
layout: 'classic',
defaultHttpClient: {
targetKey: 'javascript',
clientKey: 'fetch',
},
spec: {
url: '/api/doc',
},
}),
);
}

View file

@ -0,0 +1 @@
export * from '../../users/tables/users.table';

View file

@ -0,0 +1,30 @@
import pg from 'pg';
import * as drizzleSchema from './drizzle-schema';
import { inject, injectable } from '@needle-di/core';
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
import { ConfigService } from '../../common/configs/config.service';
@injectable()
export class DrizzleService {
public pool: pg.Pool;
public db: NodePgDatabase<typeof drizzleSchema>;
public schema: typeof drizzleSchema = drizzleSchema;
constructor(private configService = inject(ConfigService)) {
const pool = new pg.Pool({
user: this.configService.envs.DATABASE_USER,
password: this.configService.envs.DATABASE_PASSWORD,
host: this.configService.envs.DATABASE_HOST,
port: Number(this.configService.envs.DATABASE_PORT).valueOf(),
database: this.configService.envs.DATABASE_DB,
ssl: false,
max: this.configService.envs.DB_MIGRATING || this.configService.envs.DB_SEEDING ? 1 : undefined,
});
this.pool = pool;
this.db = drizzle({
client: pool,
casing: 'snake_case',
schema: drizzleSchema,
logger: this.configService.envs.ENV !== 'prod',
});
}
}

View file

@ -0,0 +1,33 @@
import { Redis } from 'ioredis';
import { inject, injectable } from '@needle-di/core';
import { ConfigService } from '../../common/configs/config.service';
@injectable()
export class RedisService {
public redis: Redis;
constructor(private configService = inject(ConfigService)) {
this.redis = new Redis(this.configService.envs.REDIS_URL);
}
async get(data: { prefix: string; key: string }): Promise<string | null> {
return this.redis.get(`${data.prefix}:${data.key}`);
}
async set(data: { prefix: string; key: string; value: string }): Promise<void> {
await this.redis.set(`${data.prefix}:${data.key}`, data.value);
}
async delete(data: { prefix: string; key: string }): Promise<void> {
await this.redis.del(`${data.prefix}:${data.key}`);
}
async setWithExpiry(data: {
prefix: string;
key: string;
value: string;
expiry: number;
}): Promise<void> {
await this.redis.set(`${data.prefix}:${data.key}`, data.value, 'EX', Math.floor(data.expiry));
}
}

Some files were not shown because too many files have changed in this diff Show more