feat: overengineered form and db schema

This commit is contained in:
2026-01-09 17:00:40 +01:00
parent 91556378b9
commit 76a6a77f1d
10 changed files with 555 additions and 66 deletions

View File

@@ -1,3 +1,4 @@
# helium
effortless webrtc screensharing
effortless webrtc screensharing

View File

@@ -1,19 +1,42 @@
<script lang="ts" setup>
import type { ToasterProps } from "vue-sonner"
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, XIcon } from "lucide-vue-next"
import { Toaster as Sonner } from "vue-sonner"
import { cn } from "@/lib/utils"
const props = defineProps<ToasterProps>()
</script>
<template>
<Sonner
class="toaster group"
v-bind="props"
:class="cn('toaster group', props.class)"
:style="{
'--normal-bg': 'var(--popover)',
'--normal-text': 'var(--popover-foreground)',
'--normal-border': 'var(--border)',
'--border-radius': 'var(--radius)',
}"
/>
v-bind="props"
>
<template #success-icon>
<CircleCheckIcon class="size-4" />
</template>
<template #info-icon>
<InfoIcon class="size-4" />
</template>
<template #warning-icon>
<TriangleAlertIcon class="size-4" />
</template>
<template #error-icon>
<OctagonXIcon class="size-4" />
</template>
<template #loading-icon>
<div>
<Loader2Icon class="size-4 animate-spin" />
</div>
</template>
<template #close-icon>
<XIcon class="size-4" />
</template>
</Sonner>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import SignInDialog from '~/components/app/SignInDialog.vue';
import ThemeDropdown from '~/components/ui/ThemeDropdown.vue';
import SignInDialog from "~/components/app/SignInDialog.vue";
import ThemeDropdown from "~/components/ui/ThemeDropdown.vue";
import "vue-sonner/style.css";
import { Toaster } from "@/components/ui/sonner";
</script>
<template>
@@ -17,5 +19,6 @@ import ThemeDropdown from '~/components/ui/ThemeDropdown.vue';
</ClientOnly>
</header>
<slot />
<Toaster />
</div>
</template>

View File

@@ -1,23 +1,57 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import {
boolean,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
export const peers = pgTable('peers', {
id: text('id').primaryKey(),
lastSeen: timestamp('last_seen').notNull().defaultNow(),
export const peers = pgTable("peers", {
id: text("id").primaryKey(),
lastSeen: timestamp("last_seen").notNull().defaultNow(),
});
export const rooms = pgTable('rooms', {
id: text('id').primaryKey(),
broadcaster: text('broadcaster').notNull().references(() => peers.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const roomViewers = pgTable('room_viewers', {
id: uuid('id').primaryKey().defaultRandom(),
roomId: text('room_id')
export const rooms = pgTable("rooms", {
id: text("id").primaryKey(),
broadcaster: text("broadcaster")
.notNull()
.references(() => rooms.id, { onDelete: 'cascade' }),
viewerId: text('viewer_id')
.notNull()
.references(() => peers.id, { onDelete: 'cascade' }),
joinedAt: timestamp('joined_at').notNull().defaultNow(),
.references(() => peers.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const roomViewers = pgTable("room_viewers", {
id: uuid("id").primaryKey().defaultRandom(),
roomId: text("room_id")
.notNull()
.references(() => rooms.id, { onDelete: "cascade" }),
viewerId: text("viewer_id")
.notNull()
.references(() => peers.id, { onDelete: "cascade" }),
joinedAt: timestamp("joined_at").notNull().defaultNow(),
});
export const presets = pgTable("presets", {
id: uuid("id").primaryKey().defaultRandom(),
createdBy: text("created_by").notNull(),
name: text("name").notNull(),
iceServers: text("ice_servers").notNull(), // stringified json obv
shareable: boolean("shareable").notNull().default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const presetUsers = pgTable(
"preset_users",
{
id: uuid("id").primaryKey().defaultRandom(),
presetId: uuid("preset_id")
.notNull()
.references(() => presets.id, { onDelete: "cascade" }),
userId: text("user_id").notNull(),
isDefault: boolean("is_default").notNull().default(false),
addedAt: timestamp("added_at").notNull().defaultNow(),
},
(table) => ({
uniquePresetUser: uniqueIndex().on(table.presetId, table.userId),
}),
);

View File

@@ -1,26 +1,26 @@
<template>
<div class="flex flex-col gap-6 max-w-2xl m-auto">
<form id="form-tanstack-input" @submit.prevent="form.handleSubmit">
<div class="flex flex-col max-w-2xl m-auto">
<form
id="form-tanstack-input"
@submit.prevent="form.handleSubmit"
class="space-y-6"
>
<FieldGroup>
<form.Field v-slot="{ field }" name="username">
<form.Field v-slot="{ field }" name="name">
<Field :data-invalid="isInvalid(field)">
<FieldLabel for="form-tanstack-input-username">
Username
Preset name
</FieldLabel>
<Input
id="form-tanstack-input-username"
:name="field.name"
:model-value="field.state.value"
:aria-invalid="isInvalid(field)"
placeholder="shadcn"
autocomplete="username"
placeholder="My ICE Preset"
autocomplete="off"
@blur="field.handleBlur"
@input="field.handleChange($event.target.value)"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10
characters. Must only contain letters, numbers, and underscores.
</FieldDescription>
<FieldError
v-if="isInvalid(field)"
:errors="field.state.meta.errors"
@@ -28,22 +28,36 @@
</Field>
</form.Field>
</FieldGroup>
<form.Field v-slot="{ field }" name="iceServers">
<Field :data-invalid="isInvalid(field)">
<FieldLabel>Ice Servers (JSON)</FieldLabel>
<div
class="h-96 w-full border rounded-md overflow-hidden focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] transition"
>
<ClientOnly>
<MonacoEditor
:model-value="field.state.value"
:options="editorOptions"
class="h-full w-full"
lang="json"
@update:model-value="field.handleChange"
@blur="field.handleBlur"
/>
</ClientOnly>
</div>
<FieldError
v-if="isInvalid(field)"
:errors="field.state.meta.errors"
/>
</Field>
</form.Field>
<Field orientation="horizontal">
<!--<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>-->
<Button type="submit" form="form-tanstack-input">Save</Button>
</Field>
</form>
<div class="h-96 w-full border rounded-md overflow-hidden">
<ClientOnly>
<MonacoEditor
:options="editorOptions"
class="h-full w-full"
lang="json"
/>
</ClientOnly>
</div>
<Field orientation="horizontal">
<Button type="button" variant="outline" @click="form.reset()">
Reset
</Button>
<Button type="submit" form="form-tanstack-input"> Save </Button>
</Field>
</div>
</template>
@@ -80,37 +94,134 @@ if (import.meta.client) {
}
const formSchema = z.object({
username: z
name: z
.string()
.min(3, "Username must be at least 3 characters.")
.max(10, "Username must be at most 10 characters.")
.regex(
/^\w+$/,
"Username can only contain letters, numbers, and underscores.",
),
.min(3, "Name must be at least 3 characters.")
.max(20, "Name must be at most 20 characters."),
iceServers: z.string().superRefine((val, ctx) => {
// below code is ai generated. i am not writing validation myself istg
try {
const parsed = JSON.parse(val);
if (!Array.isArray(parsed)) {
ctx.addIssue({
code: "custom",
message: "Must be a JSON array",
});
return;
}
// Validate each ICE server object
parsed.forEach((item, index) => {
if (typeof item !== "object" || item === null) {
ctx.addIssue({
code: "custom",
message: `Item ${index}: must be an object`,
});
return;
}
// Validate urls field - can be string or array of strings
const { urls } = item;
if (!urls) {
ctx.addIssue({
code: "custom",
message: `Item ${index}: 'urls' is required`,
});
return;
}
const urlsList = Array.isArray(urls) ? urls : [urls];
if (!Array.isArray(urls) && typeof urls !== "string") {
ctx.addIssue({
code: "custom",
message: `Item ${index}: 'urls' must be a string or array of strings`,
});
return;
}
// Validate each URL in the urls list
urlsList.forEach((url, urlIndex) => {
if (typeof url !== "string") {
ctx.addIssue({
code: "custom",
message: `Item ${index}: urls[${urlIndex}] must be a string`,
});
return;
}
// Validate STUN/TURN URL format (RFC 8829)
const isValidStunUrl = /^stuns?:.+/.test(url);
const isValidTurnUrl = /^turns?:.+/.test(url);
if (!isValidStunUrl && !isValidTurnUrl) {
ctx.addIssue({
code: "custom",
message: `Item ${index}: urls[${urlIndex}] must be a valid STUN (stun:) or TURN (turn:/turns:) URL`,
});
}
});
// Validate optional fields
if (item.username !== undefined && typeof item.username !== "string") {
ctx.addIssue({
code: "custom",
message: `Item ${index}: 'username' must be a string`,
});
}
if (
item.credential !== undefined &&
typeof item.credential !== "string"
) {
ctx.addIssue({
code: "custom",
message: `Item ${index}: 'credential' must be a string`,
});
}
if (
item.credentialType !== undefined &&
!["password", "oauth"].includes(item.credentialType)
) {
ctx.addIssue({
code: "custom",
message: `Item ${index}: 'credentialType' must be 'password' or 'oauth'`,
});
}
});
} catch (error) {
ctx.addIssue({
code: "custom",
message: "Must be valid JSON",
});
}
}),
});
const form = useForm({
defaultValues: {
username: "",
name: "",
iceServers:
'[\n\t{ "urls": "stun:stun.l.google.com:19302" }\,\n\t{ "urls": "stun:stun1.l.google.com:19302" }\n]',
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
// Parse the JSON string back to an object for submission
const parsedValue = {
...value,
iceServers: JSON.parse(value.iceServers),
};
toast("You submitted the following values:", {
description: h(
"pre",
{
class:
"bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4",
"bg-code text-white mt-2 w-[320px] overflow-x-auto rounded-md p-4",
},
h("code", JSON.stringify(value, null, 2)),
h("code", JSON.stringify(parsedValue, null, 2)),
),
position: "bottom-right",
class: "flex flex-col gap-2",
style: {
"--border-radius": "calc(var(--radius) + 4px)",
},
});
},
});

View File

@@ -0,0 +1,19 @@
CREATE TABLE "preset_users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"preset_id" uuid NOT NULL,
"user_id" text NOT NULL,
"is_default" boolean DEFAULT false NOT NULL,
"added_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "presets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"created_by" text NOT NULL,
"name" text NOT NULL,
"ice_servers" text NOT NULL,
"shareable" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "preset_users" ADD CONSTRAINT "preset_users_preset_id_presets_id_fk" FOREIGN KEY ("preset_id") REFERENCES "public"."presets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "preset_users_preset_id_user_id_index" ON "preset_users" USING btree ("preset_id","user_id");

View File

@@ -0,0 +1,291 @@
{
"id": "2d7f156d-41b3-41bc-83a1-e2fe9e5ba17d",
"prevId": "bede7229-4dec-4922-b74e-3b2d146e8f85",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.peers": {
"name": "peers",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"last_seen": {
"name": "last_seen",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.preset_users": {
"name": "preset_users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"preset_id": {
"name": "preset_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"is_default": {
"name": "is_default",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"added_at": {
"name": "added_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"preset_users_preset_id_user_id_index": {
"name": "preset_users_preset_id_user_id_index",
"columns": [
{
"expression": "preset_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"preset_users_preset_id_presets_id_fk": {
"name": "preset_users_preset_id_presets_id_fk",
"tableFrom": "preset_users",
"tableTo": "presets",
"columnsFrom": [
"preset_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.presets": {
"name": "presets",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"created_by": {
"name": "created_by",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"ice_servers": {
"name": "ice_servers",
"type": "text",
"primaryKey": false,
"notNull": true
},
"shareable": {
"name": "shareable",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.room_viewers": {
"name": "room_viewers",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"room_id": {
"name": "room_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"viewer_id": {
"name": "viewer_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"joined_at": {
"name": "joined_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"room_viewers_room_id_rooms_id_fk": {
"name": "room_viewers_room_id_rooms_id_fk",
"tableFrom": "room_viewers",
"tableTo": "rooms",
"columnsFrom": [
"room_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"room_viewers_viewer_id_peers_id_fk": {
"name": "room_viewers_viewer_id_peers_id_fk",
"tableFrom": "room_viewers",
"tableTo": "peers",
"columnsFrom": [
"viewer_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.rooms": {
"name": "rooms",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"broadcaster": {
"name": "broadcaster",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"rooms_broadcaster_peers_id_fk": {
"name": "rooms_broadcaster_peers_id_fk",
"tableFrom": "rooms",
"tableTo": "peers",
"columnsFrom": [
"broadcaster"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1765910486192,
"tag": "0000_pale_maximus",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1767895147484,
"tag": "0001_material_puma",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
import { shadcn } from "@clerk/themes";
import monacoEditorPlugin from "vite-plugin-monaco-editor";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: "2025-07-15",

View File

@@ -0,0 +1 @@
export default defineEventHandler(() => "Test post handler");