mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
fix: unique constraint on name, and flex redesign
This commit is contained in:
@@ -35,11 +35,13 @@ export const roomViewers = pgTable("room_viewers", {
|
|||||||
export const presets = pgTable("presets", {
|
export const presets = pgTable("presets", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
createdBy: text("created_by").notNull(),
|
createdBy: text("created_by").notNull(),
|
||||||
name: text("name").notNull().unique(),
|
name: text("name").notNull(),
|
||||||
iceServers: text("ice_servers").notNull(), // stringified json obv
|
iceServers: text("ice_servers").notNull(), // stringified json obv
|
||||||
shareable: boolean("shareable").notNull().default(false),
|
shareable: boolean("shareable").notNull().default(false),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
});
|
}, (table) => ({
|
||||||
|
uniqueUserPresetName: uniqueIndex().on(table.createdBy, table.name),
|
||||||
|
}));
|
||||||
|
|
||||||
export const presetUsers = pgTable(
|
export const presetUsers = pgTable(
|
||||||
"preset_users",
|
"preset_users",
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4 min-h-[80vh]">
|
<div
|
||||||
<div v-if="!isConnected" class="flex flex-col items-center gap-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
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">
|
<div class="text-center space-y-2">
|
||||||
<h1 class="text-4xl font-bold tracking-tight">helium</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<app-code-input />
|
<app-code-input />
|
||||||
@@ -20,7 +27,7 @@
|
|||||||
:class="[
|
:class="[
|
||||||
isConnected
|
isConnected
|
||||||
? 'fixed inset-0 z-50 w-full h-full bg-black'
|
? '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'
|
: 'relative w-full max-w-3xl aspect-video rounded-xl overflow-hidden border shadow-sm bg-muted/50',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<!-- Status Overlay -->
|
<!-- Status Overlay -->
|
||||||
@@ -34,9 +41,16 @@
|
|||||||
Enter another code
|
Enter another code
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="viewerStore.connectionStatus !== 'waiting for a code'" class="space-y-4">
|
<div
|
||||||
<div class="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full mx-auto" />
|
v-else-if="viewerStore.connectionStatus !== 'waiting for a code'"
|
||||||
<p class="text-sm font-medium text-muted-foreground">{{ viewerStore.connectionStatus }}</p>
|
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>
|
</div>
|
||||||
<p v-else class="text-muted-foreground/50 text-sm">
|
<p v-else class="text-muted-foreground/50 text-sm">
|
||||||
enter code to join stream
|
enter code to join stream
|
||||||
@@ -254,7 +268,7 @@ function cleanupViewing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear code
|
// Clear code
|
||||||
viewerStore.code = '';
|
viewerStore.code = "";
|
||||||
|
|
||||||
// Reset connection status
|
// Reset connection status
|
||||||
viewerStore.setConnectionStatus("disconnected");
|
viewerStore.setConnectionStatus("disconnected");
|
||||||
@@ -280,7 +294,7 @@ function toggleFullscreen() {
|
|||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
viewerStore.resetDisconnected();
|
viewerStore.resetDisconnected();
|
||||||
viewerStore.setConnectionStatus('waiting for a code');
|
viewerStore.setConnectionStatus("waiting for a code");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on component unmount
|
// Cleanup on component unmount
|
||||||
@@ -302,3 +316,4 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 flex justify-end gap-3">
|
<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 variant="outline" @click="navigateTo('/')">Cancel</Button>
|
||||||
<Button @click="importPreset" :disabled="isImporting">
|
<Button @click="importPreset" :disabled="isImporting">
|
||||||
{{ isImporting ? "Importing..." : "Import Preset" }}
|
{{ isImporting ? "Importing..." : "Import Preset" }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div v-else-if="!response" class="text-center text-muted-foreground">
|
<div v-else-if="!response" class="text-center text-muted-foreground">
|
||||||
@@ -102,6 +110,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import type { PresetShareResponse } from "~/lib/types/PresetShareResponse";
|
import type { PresetShareResponse } from "~/lib/types/PresetShareResponse";
|
||||||
import { toast } from "vue-sonner";
|
import { toast } from "vue-sonner";
|
||||||
|
|
||||||
@@ -112,6 +121,7 @@ const { data: response, pending } = useFetch<PresetShareResponse>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isImporting = ref(false);
|
const isImporting = ref(false);
|
||||||
|
const setAsDefault = ref(false);
|
||||||
|
|
||||||
const parsedIceServers = computed(() => {
|
const parsedIceServers = computed(() => {
|
||||||
if (!response.value?.data.iceServers) return [];
|
if (!response.value?.data.iceServers) return [];
|
||||||
@@ -126,6 +136,9 @@ const importPreset = async () => {
|
|||||||
`/api/presets/${route.params.id}/import`,
|
`/api/presets/${route.params.id}/import`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
setAsDefault: setAsDefault.value,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
2
drizzle/0003_large_nocturne.sql
Normal file
2
drizzle/0003_large_nocturne.sql
Normal 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");
|
||||||
313
drizzle/meta/0003_snapshot.json
Normal file
313
drizzle/meta/0003_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1767985505802,
|
"when": 1767985505802,
|
||||||
"tag": "0002_jazzy_onslaught",
|
"tag": "0002_jazzy_onslaught",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1768337998925,
|
||||||
|
"tag": "0003_large_nocturne",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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 { db } from "~/lib/db/index";
|
||||||
import * as schema from "~/lib/db/schema";
|
import * as schema from "~/lib/db/schema";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = getRouterParam(event, "id");
|
const id = getRouterParam(event, "id");
|
||||||
const { isAuthenticated, userId } = event.context.auth();
|
const { isAuthenticated, userId } = event.context.auth();
|
||||||
|
const body = await readBody<{ setAsDefault?: boolean }>(event);
|
||||||
|
|
||||||
if (!isAuthenticated || !userId) {
|
if (!isAuthenticated || !userId) {
|
||||||
setResponseStatus(event, 401);
|
setResponseStatus(event, 401);
|
||||||
@@ -59,9 +60,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
await db.insert(schema.presetUsers).values({
|
await db.insert(schema.presetUsers).values({
|
||||||
presetId: id,
|
presetId: id,
|
||||||
userId: userId,
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Preset imported successfully",
|
message: "Preset imported successfully",
|
||||||
|
|||||||
Reference in New Issue
Block a user