fix: unique constraint on name, and flex redesign

This commit is contained in:
2026-01-13 22:02:40 +01:00
parent 3f92dd2226
commit c70fb7f06e
7 changed files with 393 additions and 35 deletions

View File

@@ -35,11 +35,13 @@ export const roomViewers = pgTable("room_viewers", {
export const presets = pgTable("presets", {
id: uuid("id").primaryKey().defaultRandom(),
createdBy: text("created_by").notNull(),
name: text("name").notNull().unique(),
name: text("name").notNull(),
iceServers: text("ice_servers").notNull(), // stringified json obv
shareable: boolean("shareable").notNull().default(false),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
}, (table) => ({
uniqueUserPresetName: uniqueIndex().on(table.createdBy, table.name),
}));
export const presetUsers = pgTable(
"preset_users",

View File

@@ -1,13 +1,20 @@
<template>
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4 min-h-[80vh]">
<div v-if="!isConnected" class="flex flex-col items-center gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div
class="flex flex-col md:flex-row items-center justify-center gap-6 mt-10 px-4 min-h-[80vh]"
>
<div
v-if="!isConnected"
class="flex flex-col items-center gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500"
>
<div class="text-center space-y-2">
<h1 class="text-4xl font-bold tracking-tight">helium</h1>
<p class="text-muted-foreground text-lg">effortless screensharing powered by webrtc</p>
<p class="text-muted-foreground text-lg">
effortless screensharing powered by webrtc
</p>
</div>
<app-code-input />
<NuxtLink to="/stream">
<Button variant="link" class="text-muted-foreground hover:text-primary">
host instead?
@@ -15,12 +22,12 @@
</NuxtLink>
</div>
<div
<div
class="video transition-all duration-500 ease-in-out"
:class="[
isConnected
? 'fixed inset-0 z-50 w-full h-full bg-black'
: 'relative w-full max-w-3xl aspect-video rounded-xl overflow-hidden border shadow-sm bg-muted/50'
isConnected
? 'fixed inset-0 z-50 w-full h-full bg-black'
: 'relative w-full max-w-3xl aspect-video rounded-xl overflow-hidden border shadow-sm bg-muted/50',
]"
>
<!-- Status Overlay -->
@@ -34,9 +41,16 @@
Enter another code
</Button>
</div>
<div v-else-if="viewerStore.connectionStatus !== 'waiting for a code'" class="space-y-4">
<div class="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full mx-auto" />
<p class="text-sm font-medium text-muted-foreground">{{ viewerStore.connectionStatus }}</p>
<div
v-else-if="viewerStore.connectionStatus !== 'waiting for a code'"
class="space-y-4"
>
<div
class="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full mx-auto"
/>
<p class="text-sm font-medium text-muted-foreground">
{{ viewerStore.connectionStatus }}
</p>
</div>
<p v-else class="text-muted-foreground/50 text-sm">
enter code to join stream
@@ -54,13 +68,13 @@
/>
<!-- Connected Controls Overlay -->
<div
v-if="isConnected"
<div
v-if="isConnected"
class="absolute top-0 left-0 right-0 p-4 flex justify-between items-start opacity-0 hover:opacity-100 transition-opacity bg-gradient-to-b from-black/50 to-transparent"
>
<Button
variant="destructive"
size="lg"
<Button
variant="destructive"
size="lg"
class="gap-2 shadow-lg"
@click="cleanupViewing"
>
@@ -68,9 +82,9 @@
Disconnect
</Button>
<Button
variant="secondary"
size="lg"
<Button
variant="secondary"
size="lg"
class="gap-2 shadow-lg"
@click="toggleFullscreen"
>
@@ -252,14 +266,14 @@ function cleanupViewing() {
if (videofeedRef.value) {
videofeedRef.value.srcObject = null;
}
// Clear code
viewerStore.code = '';
viewerStore.code = "";
// Reset connection status
viewerStore.setConnectionStatus("disconnected");
isConnected.value = false;
// Exit fullscreen if active
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
@@ -268,7 +282,7 @@ function cleanupViewing() {
function toggleFullscreen() {
if (!videofeedRef.value) return;
if (!document.fullscreenElement) {
videofeedRef.value.requestFullscreen().catch((err) => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
@@ -280,7 +294,7 @@ function toggleFullscreen() {
function handleReset() {
viewerStore.resetDisconnected();
viewerStore.setConnectionStatus('waiting for a code');
viewerStore.setConnectionStatus("waiting for a code");
}
// Cleanup on component unmount
@@ -301,4 +315,5 @@ onMounted(() => {
window.removeEventListener("beforeunload", handleBeforeUnload);
});
});
</script>
</script>

View File

@@ -66,11 +66,19 @@
</div>
</div>
<div class="mt-8 flex justify-end gap-3">
<Button variant="outline" @click="navigateTo('/')">Cancel</Button>
<Button @click="importPreset" :disabled="isImporting">
{{ isImporting ? "Importing..." : "Import Preset" }}
</Button>
<div class="mt-8 space-y-4">
<div class="flex items-center justify-between p-3 bg-muted rounded-lg">
<label class="text-sm font-medium cursor-pointer"
>Activate by default</label
>
<Switch v-model="setAsDefault" />
</div>
<div class="flex justify-end gap-3">
<Button variant="outline" @click="navigateTo('/')">Cancel</Button>
<Button @click="importPreset" :disabled="isImporting">
{{ isImporting ? "Importing..." : "Import Preset" }}
</Button>
</div>
</div>
</CardContent>
</Card>
@@ -102,6 +110,7 @@ import {
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import type { PresetShareResponse } from "~/lib/types/PresetShareResponse";
import { toast } from "vue-sonner";
@@ -112,6 +121,7 @@ const { data: response, pending } = useFetch<PresetShareResponse>(
);
const isImporting = ref(false);
const setAsDefault = ref(false);
const parsedIceServers = computed(() => {
if (!response.value?.data.iceServers) return [];
@@ -126,6 +136,9 @@ const importPreset = async () => {
`/api/presets/${route.params.id}/import`,
{
method: "POST",
body: {
setAsDefault: setAsDefault.value,
},
},
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE "presets" DROP CONSTRAINT "presets_name_unique";--> statement-breakpoint
CREATE UNIQUE INDEX "presets_created_by_name_index" ON "presets" USING btree ("created_by","name");

View File

@@ -0,0 +1,313 @@
{
"id": "80084ee5-2522-47c7-aedd-846b3d820e8e",
"prevId": "8c94aa55-1f21-4db7-9adb-26c17d4075a5",
"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": {
"presets_created_by_name_index": {
"name": "presets_created_by_name_index",
"columns": [
{
"expression": "created_by",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"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

@@ -22,6 +22,13 @@
"when": 1767985505802,
"tag": "0002_jazzy_onslaught",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1768337998925,
"tag": "0003_large_nocturne",
"breakpoints": true
}
]
}

View File

@@ -1,10 +1,11 @@
import { getPresetById, userHasPresetAccess } from "~/lib/utils/presetsDb";
import { getPresetById, userHasPresetAccess, setPresetAsDefault } from "~/lib/utils/presetsDb";
import { db } from "~/lib/db/index";
import * as schema from "~/lib/db/schema";
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const { isAuthenticated, userId } = event.context.auth();
const body = await readBody<{ setAsDefault?: boolean }>(event);
if (!isAuthenticated || !userId) {
setResponseStatus(event, 401);
@@ -59,9 +60,14 @@ export default defineEventHandler(async (event) => {
await db.insert(schema.presetUsers).values({
presetId: id,
userId: userId,
isDefault: false,
isDefault: body.setAsDefault ?? false,
});
// If setAsDefault is true and this is the first import, set it as default
if (body.setAsDefault) {
await setPresetAsDefault(id, userId);
}
return {
success: true,
message: "Preset imported successfully",