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", {
|
||||
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",
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<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 />
|
||||
@@ -20,7 +27,7 @@
|
||||
: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'
|
||||
: '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
|
||||
@@ -254,7 +268,7 @@ function cleanupViewing() {
|
||||
}
|
||||
|
||||
// Clear code
|
||||
viewerStore.code = '';
|
||||
viewerStore.code = "";
|
||||
|
||||
// Reset connection status
|
||||
viewerStore.setConnectionStatus("disconnected");
|
||||
@@ -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
|
||||
@@ -302,3 +316,4 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -66,12 +66,20 @@
|
||||
</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 @click="importPreset" :disabled="isImporting">
|
||||
{{ isImporting ? "Importing..." : "Import Preset" }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div v-else-if="!response" class="text-center text-muted-foreground">
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
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,
|
||||
"tag": "0002_jazzy_onslaught",
|
||||
"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 * 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",
|
||||
|
||||
Reference in New Issue
Block a user