diff --git a/app/components/app/EditPresetDialog.vue b/app/components/app/EditPresetDialog.vue new file mode 100644 index 0000000..b470e3e --- /dev/null +++ b/app/components/app/EditPresetDialog.vue @@ -0,0 +1,54 @@ + + + diff --git a/app/components/app/PresetForm.vue b/app/components/app/PresetForm.vue new file mode 100644 index 0000000..4f0e2e3 --- /dev/null +++ b/app/components/app/PresetForm.vue @@ -0,0 +1,169 @@ + + + diff --git a/app/components/ui/badge/Badge.vue b/app/components/ui/badge/Badge.vue new file mode 100644 index 0000000..d894dfe --- /dev/null +++ b/app/components/ui/badge/Badge.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/components/ui/badge/index.ts b/app/components/ui/badge/index.ts new file mode 100644 index 0000000..bbc0dfa --- /dev/null +++ b/app/components/ui/badge/index.ts @@ -0,0 +1,26 @@ +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" + +export { default as Badge } from "./Badge.vue" + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) +export type BadgeVariants = VariantProps diff --git a/app/components/ui/card/Card.vue b/app/components/ui/card/Card.vue new file mode 100644 index 0000000..f5a0707 --- /dev/null +++ b/app/components/ui/card/Card.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/components/ui/card/CardAction.vue b/app/components/ui/card/CardAction.vue new file mode 100644 index 0000000..c91638b --- /dev/null +++ b/app/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardContent.vue b/app/components/ui/card/CardContent.vue new file mode 100644 index 0000000..dfbc552 --- /dev/null +++ b/app/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardDescription.vue b/app/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..71c1b8d --- /dev/null +++ b/app/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardFooter.vue b/app/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..9e3739e --- /dev/null +++ b/app/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardHeader.vue b/app/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..4fe4da4 --- /dev/null +++ b/app/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardTitle.vue b/app/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..5f479e7 --- /dev/null +++ b/app/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/index.ts b/app/components/ui/card/index.ts new file mode 100644 index 0000000..1627758 --- /dev/null +++ b/app/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue" +export { default as CardAction } from "./CardAction.vue" +export { default as CardContent } from "./CardContent.vue" +export { default as CardDescription } from "./CardDescription.vue" +export { default as CardFooter } from "./CardFooter.vue" +export { default as CardHeader } from "./CardHeader.vue" +export { default as CardTitle } from "./CardTitle.vue" diff --git a/app/pages/presets/index.vue b/app/pages/presets/index.vue index d337034..6328801 100644 --- a/app/pages/presets/index.vue +++ b/app/pages/presets/index.vue @@ -1,9 +1,158 @@ diff --git a/app/pages/presets/new.vue b/app/pages/presets/new.vue index b3e4478..06e03db 100644 --- a/app/pages/presets/new.vue +++ b/app/pages/presets/new.vue @@ -1,157 +1,11 @@ diff --git a/server/api/presets/[id].delete.ts b/server/api/presets/[id].delete.ts new file mode 100644 index 0000000..4becd40 --- /dev/null +++ b/server/api/presets/[id].delete.ts @@ -0,0 +1,34 @@ +import { eq, and } from "drizzle-orm"; +import { db } from "~/lib/db"; +import { presets, presetUsers } from "~/lib/db/schema"; + +export default defineEventHandler(async (event) => { + const { isAuthenticated, userId } = event.context.auth(); + + if (!isAuthenticated || !userId) { + throw createError({ statusCode: 401, statusMessage: "Unauthorized" }); + } + + const id = getRouterParam(event, "id"); + if (!id) { + throw createError({ statusCode: 400, statusMessage: "Missing preset ID" }); + } + + // Check if the user is the creator of the preset + const preset = await db.query.presets.findFirst({ + where: eq(presets.id, id), + }); + + if (!preset) { + throw createError({ statusCode: 404, statusMessage: "Preset not found" }); + } + + if (preset.createdBy !== userId) { + throw createError({ statusCode: 403, statusMessage: "Forbidden: You can only delete your own presets" }); + } + + // Delete the preset (cascades to presetUsers) + await db.delete(presets).where(eq(presets.id, id)); + + return { success: true }; +}); diff --git a/server/api/presets/[id].get.ts b/server/api/presets/[id].get.ts new file mode 100644 index 0000000..496cf2d --- /dev/null +++ b/server/api/presets/[id].get.ts @@ -0,0 +1,39 @@ +import { eq } from "drizzle-orm"; +import { db } from "~/lib/db"; +import { presets, presetUsers } from "~/lib/db/schema"; + +export default defineEventHandler(async (event) => { + const { isAuthenticated, userId } = event.context.auth(); + + if (!isAuthenticated || !userId) { + throw createError({ statusCode: 401, statusMessage: "Unauthorized" }); + } + + const id = getRouterParam(event, "id"); + if (!id) { + throw createError({ statusCode: 400, statusMessage: "Missing preset ID" }); + } + + // Fetch the preset + const preset = await db.query.presets.findFirst({ + where: eq(presets.id, id), + }); + + if (!preset) { + throw createError({ statusCode: 404, statusMessage: "Preset not found" }); + } + + // Check if user has access (either creator or has it in their presetUsers) + const userPreset = await db.query.presetUsers.findFirst({ + where: eq(presetUsers.presetId, id), + }); + + if (preset.createdBy !== userId && (!userPreset || userPreset.userId !== userId)) { + throw createError({ statusCode: 403, statusMessage: "Forbidden" }); + } + + return { + success: true, + data: preset, + }; +}); diff --git a/server/api/presets/[id].put.ts b/server/api/presets/[id].put.ts new file mode 100644 index 0000000..2e4e39b --- /dev/null +++ b/server/api/presets/[id].put.ts @@ -0,0 +1,59 @@ +import { eq, and } from "drizzle-orm"; +import { db } from "~/lib/db"; +import { presets, presetUsers } from "~/lib/db/schema"; + +export default defineEventHandler(async (event) => { + const { isAuthenticated, userId } = event.context.auth(); + + if (!isAuthenticated || !userId) { + throw createError({ statusCode: 401, statusMessage: "Unauthorized" }); + } + + const id = getRouterParam(event, "id"); + if (!id) { + throw createError({ statusCode: 400, statusMessage: "Missing preset ID" }); + } + + const body = await readBody(event); + + // Verify ownership + const preset = await db.query.presets.findFirst({ + where: eq(presets.id, id), + }); + + if (!preset) { + throw createError({ statusCode: 404, statusMessage: "Preset not found" }); + } + + if (preset.createdBy !== userId) { + throw createError({ statusCode: 403, statusMessage: "Forbidden: You can only edit your own presets" }); + } + + // Update preset + await db.update(presets) + .set({ + name: body.name, + iceServers: JSON.stringify(body.iceServers), + }) + .where(eq(presets.id, id)); + + // Update default status in presetUsers + if (body.default !== undefined) { + // If setting as default, first unset all other defaults for this user + if (body.default) { + await db.update(presetUsers) + .set({ isDefault: false }) + .where(eq(presetUsers.userId, userId)); + } + + // Update the default status for this preset + await db.update(presetUsers) + .set({ isDefault: body.default }) + .where(and( + eq(presetUsers.presetId, id), + eq(presetUsers.userId, userId) + )); + } + + return { success: true }; +});