diff --git a/api/src/controllers/seed/index.ts b/api/src/controllers/seed/index.ts index b2208e86..1b72dbf4 100644 --- a/api/src/controllers/seed/index.ts +++ b/api/src/controllers/seed/index.ts @@ -1,8 +1,11 @@ -import Elysia from "elysia"; +import { and, eq } from "drizzle-orm"; +import Elysia, { t } from "elysia"; +import { db } from "~/db"; +import { shows } from "~/db/schema"; import { KError } from "~/models/error"; import { SeedMovie } from "~/models/movie"; import { SeedSerie } from "~/models/serie"; -import { Resource } from "~/models/utils"; +import { isUuid, Resource } from "~/models/utils"; import { comment } from "~/utils"; import { SeedMovieResponse, seedMovie } from "./movies"; import { SeedSerieResponse, seedSerie } from "./series"; @@ -77,4 +80,121 @@ export const seed = new Elysia() 422: KError, }, }, + ) + .delete( + "/movies/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "movie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No movie found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["movies"], + description: "Delete a movie by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the movie to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No movie found with the given id or slug.", + }, + }, + }, + ) + .delete( + "/series/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "serie"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No serie found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["series"], + description: "Delete a serie by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the serie to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No serie found with the given id or slug.", + }, + }, + }, + ) + .delete( + "/collections/:id", + async ({ params: { id }, status }) => { + const [deleted] = await db + .delete(shows) + .where( + and( + eq(shows.kind, "collection"), + isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), + ), + ) + .returning({ id: shows.id, slug: shows.slug }); + if (!deleted) { + return status(404, { + status: 404, + message: "No collection found with the given id or slug.", + }); + } + return deleted; + }, + { + detail: { + tags: ["collections"], + description: "Delete a collection by id or slug.", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the collection to delete.", + }), + }), + response: { + 200: Resource(), + 404: { + ...KError, + description: "No collection found with the given id or slug.", + }, + }, + }, ); diff --git a/front/bun.lock b/front/bun.lock index 854d791d..f0689d12 100644 --- a/front/bun.lock +++ b/front/bun.lock @@ -57,7 +57,6 @@ "react-native-worklets": "0.7.2", "react-tooltip": "^5.30.0", "react-use-websocket": "^4.13.0", - "sweetalert2": "^11.26.22", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tsx": "^4.21.0", @@ -1476,8 +1475,6 @@ "svgo": ["svgo@3.3.3", "", { "dependencies": { "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0", "sax": "^1.5.0" }, "bin": "./bin/svgo" }, "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng=="], - "sweetalert2": ["sweetalert2@11.26.24", "", {}, "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg=="], - "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="], diff --git a/front/package.json b/front/package.json index 70914f1d..27ec69cc 100644 --- a/front/package.json +++ b/front/package.json @@ -68,7 +68,7 @@ "react-native-worklets": "0.7.2", "react-tooltip": "^5.30.0", "react-use-websocket": "^4.13.0", - "sweetalert2": "^11.26.22", + "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", "tsx": "^4.21.0", diff --git a/front/public/translations/en.json b/front/public/translations/en.json index d39cfdfd..608a6f1b 100644 --- a/front/public/translations/en.json +++ b/front/public/translations/en.json @@ -117,6 +117,7 @@ "prev-page": "Previous page", "next-page": "Next page", "delete": "Delete", + "delete-name": "Delete {{name}}", "cancel": "Cancel", "more": "More", "expand": "Expand", diff --git a/front/src/primitives/alert.tsx b/front/src/primitives/alert.tsx index c61d3daf..deadc9a0 100644 --- a/front/src/primitives/alert.tsx +++ b/front/src/primitives/alert.tsx @@ -5,14 +5,13 @@ import { type AlertOptions, Alert as RNAlert, } from "react-native"; -import type { SweetAlertIcon } from "sweetalert2"; export interface ExtendedAlertStatic { alert: ( title: string, message?: string, buttons?: AlertButton[], - options?: AlertOptions & { icon?: SweetAlertIcon }, + options?: AlertOptions, ) => void; } diff --git a/front/src/primitives/alert.web.tsx b/front/src/primitives/alert.web.tsx index 15d96069..cff12eca 100644 --- a/front/src/primitives/alert.web.tsx +++ b/front/src/primitives/alert.web.tsx @@ -1,7 +1,72 @@ -// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681 - +import { createRoot } from "react-dom/client"; import type { AlertButton, AlertOptions } from "react-native"; -import Swal, { type SweetAlertIcon } from "sweetalert2"; +import { Pressable, View } from "react-native"; +import { cn } from "~/utils"; +import { Heading, P } from "./text"; + +const AlertDialog = ({ + title, + message, + buttons, + onDismiss, +}: { + title: string; + message?: string; + buttons: AlertButton[]; + onDismiss: () => void; +}) => { + return ( + + e.preventDefault()} + > + + {title} + {message && {message}} + + + {buttons.map((button, i) => ( + { + onDismiss(); + button.onPress?.(); + }} + className={cn( + "rounded px-4 py-2", + button.style === "destructive" + ? "bg-red-700 hover:bg-red-500 focus:bg-red-500" + : button.style === "cancel" + ? "hover:bg-popover focus:bg-popover" + : "bg-accent hover:bg-accent/50 focus:bg-accent/50", + )} + > + + {button.text ?? "OK"} + + + ))} + + + + ); +}; // biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn export class Alert { @@ -9,42 +74,39 @@ export class Alert { title: string, message?: string, buttons?: AlertButton[], - options?: AlertOptions & { icon?: SweetAlertIcon }, + options?: AlertOptions, ): void { - const confirmButton = buttons - ? buttons.find((button) => button.style === "default") - : undefined; - const denyButton = buttons - ? buttons.find((button) => button.style === "destructive") - : undefined; - const cancelButton = buttons - ? buttons.find((button) => button.style === "cancel") - : undefined; + const resolvedButtons = buttons ?? [ + { text: "OK", style: "default" as const }, + ]; - Swal.fire({ - title: title, - text: message, - showConfirmButton: !!confirmButton, - showDenyButton: !!denyButton, - showCancelButton: !!cancelButton, - confirmButtonText: confirmButton?.text, - denyButtonText: denyButton?.text, - cancelButtonText: cancelButton?.text, - icon: options?.icon, - }).then((result) => { - if (result.isConfirmed) { - if (confirmButton?.onPress !== undefined) { - confirmButton.onPress(); - } - } else if (result.isDenied) { - if (denyButton?.onPress !== undefined) { - denyButton.onPress(); - } - } else if (result.isDismissed) { - if (cancelButton?.onPress !== undefined) { - cancelButton.onPress(); - } - } - }); + const container = document.createElement("div"); + container.style.position = "fixed"; + container.style.inset = "0"; + container.style.zIndex = "9999"; + document.body.appendChild(container); + + const root = createRoot(container); + + const dismiss = () => { + root.unmount(); + container.remove(); + }; + + const cancelButton = resolvedButtons.find((b) => b.style === "cancel"); + + root.render( + { + if (options?.cancelable !== false) { + dismiss(); + cancelButton?.onPress?.(); + } + }} + />, + ); } } diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index 781304f9..dd802d9b 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -1,9 +1,11 @@ import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg"; +import Delete from "@material-symbols/svg-400/rounded/delete.svg"; import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg"; import Theaters from "@material-symbols/svg-400/rounded/theaters-fill.svg"; import VideoLibrary from "@material-symbols/svg-400/rounded/video_library-fill.svg"; +import { useRouter } from "expo-router"; import { Fragment } from "react"; import { useTranslation } from "react-i18next"; import { View, type ViewProps } from "react-native"; @@ -22,6 +24,7 @@ import { import type { Metadata } from "~/models/utils/metadata"; import { A, + Alert, Chip, Container, capitalize, @@ -45,7 +48,7 @@ import { usePopup, } from "~/primitives"; import { useAccount } from "~/providers/account-context"; -import { Fetch, type QueryIdentifier } from "~/query"; +import { Fetch, type QueryIdentifier, useMutation } from "~/query"; import { cn, displayRuntime, getDisplayDate } from "~/utils"; import { PartOf } from "./part-of"; @@ -72,7 +75,13 @@ const ButtonList = ({ }) => { const account = useAccount(); const { t } = useTranslation(); + const router = useRouter(); const [setPopup, closePopup] = usePopup(); + const deleteMutation = useMutation({ + method: "DELETE", + path: ["api", `${kind}s`, slug], + invalidate: ["api", "shows"], + }); // const metadataRefreshMutation = useMutation({ // method: "POST", @@ -154,6 +163,29 @@ const ButtonList = ({ icon={VideoLibrary} href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`} /> + {kind !== "collection" && } + { + Alert.alert( + t("misc.delete-name", { name }), + t("login.delete-confirmation"), + [ + { text: t("misc.cancel"), style: "cancel" }, + { + text: t("misc.delete"), + style: "destructive", + onPress: async () => { + await deleteMutation.mutateAsync(); + router.back(); + }, + }, + ], + { cancelable: true }, + ); + }} + /> {/* { { cancelable: true, userInterfaceStyle: theme as "light" | "dark", - icon: "warning", }, ); }}
{message}
+ {button.text ?? "OK"} +