diff --git a/pages/admin/metadata/games/[id]/index.vue b/components/GameEditor/Metadata.vue similarity index 80% rename from pages/admin/metadata/games/[id]/index.vue rename to components/GameEditor/Metadata.vue index b8b621b..d845fab 100644 --- a/pages/admin/metadata/games/[id]/index.vue +++ b/components/GameEditor/Metadata.vue @@ -1,11 +1,8 @@ @@ -334,7 +314,7 @@ class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-950 transition-all duration-200 hover:scale-105 hover:shadow-lg active:scale-95 sm:mt-0 sm:w-auto" @click="showAddCarouselModal = false" > - Close + {{ $t("close") }} @@ -355,7 +335,7 @@ class="inline-flex items-center gap-x-1.5 rounded-md bg-blue-600 px-1.5 py-0.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all duration-200 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" @click="() => insertImageAtCursor(image)" > - Insert + {{ $t("insert") }} @@ -363,7 +343,7 @@ v-if="game.mImageLibraryObjectIds.length == 0" class="text-zinc-400 col-span-2" > - No images to add. + {{ $t("library.admin.game.addDescriptionNoImages") }} @@ -374,7 +354,7 @@ class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-950 transition-all duration-200 hover:scale-105 hover:shadow-lg active:scale-95 sm:mt-0 sm:w-auto" @click="showAddCarouselModal = false" > - Cancel + {{ $t("cancel") }} @@ -389,7 +369,7 @@ type="button" class="cursor-pointer relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" > - Upload + {{ $t("upload") }} Game Name{{ $t("library.admin.game.editGameName") }}
Game Description{{ $t("library.admin.game.editGameDescription") }}
- Save + {{ $t("save") }} @@ -463,12 +443,13 @@ import type { Game } from "~/prisma/client"; import { micromark } from "micromark"; import { - ArrowTopRightOnSquareIcon, CheckIcon, DocumentIcon, PencilIcon, PhotoIcon, } from "@heroicons/vue/24/solid"; +import type { SerializeObject } from "nitropack"; +import type { H3Error } from "h3"; definePageMeta({ layout: "admin", @@ -480,13 +461,16 @@ const showAddImageDescriptionModal = ref(false); const showEditCoreMetadata = ref(false); const mobileShowFinalDescription = ref(true); -const route = useRoute(); -const gameId = route.params.id.toString(); -const { game: rawGame, unimportedVersions } = await $dropFetch( - `/api/v1/admin/game?id=${encodeURIComponent(gameId)}`, -); -const game = ref(rawGame); +const game = defineModel>() as Ref>; +if (!game.value) + throw createError({ + statusCode: 500, + statusMessage: "Game not provided to editor component", + }); +const { t } = useI18n(); + +// I don't know why I split these fields off. const coreMetadataName = ref(game.value.mName); const coreMetadataDescription = ref(game.value.mShortDescription); const coreMetadataIconUrl = ref(useObject(game.value.mIconObjectId)); @@ -495,7 +479,6 @@ const coreMetadataLoading = ref(false); function coreMetadataUploadFiles(e: InputEvent) { if (coreMetadataIconUrl.value.startsWith("blob")) { - console.log("freed object URL"); URL.revokeObjectURL(coreMetadataIconUrl.value); } @@ -506,9 +489,9 @@ function coreMetadataUploadFiles(e: InputEvent) { createModal( ModalType.Notification, { - title: "Failed to upload file", - description: "Drop couldn't upload this file.", - buttonText: "Close", + title: t("errors.upload.title"), + description: t("errors.upload.description", [t("errors.unknown")]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -543,11 +526,11 @@ function coreMetadataUpdate_wrapper() { createModal( ModalType.Notification, { - title: "Failed to update metadata", - description: `Drop failed to update the game's metadata: ${ - e?.statusMessage || "An unknown error occurred. " - }`, - buttonText: "Close", + title: t("errors.game.metadata.title"), + description: t("errors.game.metadata.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -569,37 +552,42 @@ const descriptionEditor = ref(); // 0 is not loading // 1 is waiting for stop // 2 is loading -const descriptionSaving = ref(0); +enum DescriptionSavingState { + NotLoading, + Waiting, + Loading, +} +const descriptionSaving = ref( + DescriptionSavingState.NotLoading, +); let savingTimeout: undefined | NodeJS.Timeout; type PatchGameBody = Partial; watch(descriptionHTML, (_v) => { - console.log(game.value.mDescription); - descriptionSaving.value = 1; + descriptionSaving.value = DescriptionSavingState.Waiting; if (savingTimeout) clearTimeout(savingTimeout); savingTimeout = setTimeout(async () => { try { - descriptionSaving.value = 2; + descriptionSaving.value = DescriptionSavingState.Loading; await $dropFetch("/api/v1/admin/game", { method: "PATCH", body: { - id: gameId, + id: game.value.id, mDescription: game.value.mDescription, } satisfies PatchGameBody, }); - descriptionSaving.value = 0; + descriptionSaving.value = DescriptionSavingState.NotLoading; } catch (e) { createModal( ModalType.Notification, { - title: "Failed to update game description", - description: `Drop failed to update the game description: ${ - // @ts-expect-error attempt to get statusMessage on error - e?.statusMessage ?? t("errors.unknown") - }`, - buttonText: "Close", + title: t("errors.game.description.title"), + description: t("errors.game.description.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -630,7 +618,7 @@ async function updateBannerImage(id: string) { const { mBannerObjectId } = await $dropFetch("/api/v1/admin/game", { method: "PATCH", body: { - id: gameId, + id: game.value.id, mBannerObjectId: id, } satisfies PatchGameBody, }); @@ -639,12 +627,11 @@ async function updateBannerImage(id: string) { createModal( ModalType.Notification, { - title: "There an error while updating the banner image", - description: `Drop encountered an error while updating the banner image: ${ - // @ts-expect-error attempt to get statusMessage on error - e?.statusMessage ?? t("errors.unknown") - }`, - buttonText: "Close", + title: t("errors.game.banner.title"), + description: t("errors.game.banner.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -657,22 +644,20 @@ async function updateCoverImage(id: string) { const { mCoverObjectId } = await $dropFetch("/api/v1/admin/game", { method: "PATCH", body: { - id: gameId, + id: game.value.id, mCoverObjectId: id, } satisfies PatchGameBody, }); game.value.mCoverObjectId = mCoverObjectId; - coreMetadataIconUrl.value = useObject(mCoverObjectId); } catch (e) { createModal( ModalType.Notification, { - title: "There an error while updating the cover image", - description: `Drop encountered an error while updating the cover image: ${ - // @ts-expect-error attempt to get statusMessage on error - e?.statusMessage ?? t("errors.unknown") - }`, - buttonText: "Close", + title: t("errors.game.cover.title"), + description: t("errors.game.cover.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -697,12 +682,11 @@ async function deleteImage(id: string) { createModal( ModalType.Notification, { - title: "There an error while deleting the image", - description: `Drop encountered an error while deleting the image: ${ - // @ts-expect-error attempt to get statusMessage on error - e?.statusMessage ?? t("errors.unknown") - }`, - buttonText: "Close", + title: t("errors.game.deleteImage.title"), + description: t("errors.game.deleteImage.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); @@ -732,7 +716,7 @@ async function updateImageCarousel() { await $dropFetch("/api/v1/admin/game", { method: "PATCH", body: { - id: gameId, + id: game.value.id, mImageCarouselObjectIds: game.value.mImageCarouselObjectIds, } satisfies PatchGameBody, }); @@ -740,12 +724,11 @@ async function updateImageCarousel() { createModal( ModalType.Notification, { - title: "There an error while updating the image carousel", - description: `Drop encountered an error while updating image carousel: ${ - // @ts-expect-error attempt to get statusMessage on error - e?.statusMessage ?? t("errors.unknown") - }`, - buttonText: "Close", + title: t("errors.game.carousel.title"), + description: t("errors.game.carousel.description", [ + (e as H3Error)?.statusMessage ?? t("errors.unknown"), + ]), + buttonText: t("close"), }, (e, c) => c(), ); diff --git a/components/GameEditor/Version.vue b/components/GameEditor/Version.vue new file mode 100644 index 0000000..9828914 --- /dev/null +++ b/components/GameEditor/Version.vue @@ -0,0 +1,177 @@ + + + + diff --git a/i18n/locales/en_us.json b/i18n/locales/en_us.json index a7eafc9..cb513a4 100644 --- a/i18n/locales/en_us.json +++ b/i18n/locales/en_us.json @@ -25,6 +25,8 @@ "actions": "Actions", "adminTitle": "Admin Dashboard | Drop", "adminTitleTemplate": "{0} | Admin | Drop", + "title": "Drop", + "titleTemplate": "{0} | Drop", "auth": { "callback": { "authClient": "Authorize client?", @@ -79,7 +81,9 @@ "noResults": "No results", "servers": "Servers", "tags": "Tags", - "today": "Today" + "today": "Today", + "divider": "{'|'}", + "srLoading": "Loading..." }, "create": "Create", "delete": "Delete", @@ -149,6 +153,36 @@ "desc": "Drop encountered an error while updating the version: {error}", "title": "There an error while updating the version order" } + }, + "upload": { + "title": "Failed to upload file", + "description": "Drop couldn't upload the file: {0}" + }, + "game": { + "metadata": { + "title": "Failed to update metadata", + "description": "Drop failed to update the game's metadata: {0}" + }, + "description": { + "title": "Failed to update game description", + "description": "Drop failed to update the game description: {0}" + }, + "banner": { + "title": "Failed to update the banner image", + "description": "Drop failed to update the banner image: {0}" + }, + "cover": { + "title": "Failed to update the cover image", + "description": "Drop failed to update the cover image: {0}" + }, + "deleteImage": { + "title": "Failed to delete the image", + "description": "Drop failed to delete the image: {0}" + }, + "carousel": { + "title": "Failed to update image carousel", + "description": "Drop failed to update the image carousel: {0}" + } } }, "footer": { @@ -174,7 +208,6 @@ "header": { "admin": { "admin": "Admin", - "meta": "Meta", "tasks": "Tasks", "users": "Users" }, @@ -183,6 +216,79 @@ }, "highest": "highest", "home": "Home", + "users": { + "admin": { + "description": "Manage the users on your Drop instance, and configure your authentication methods.", + "authLink": "Authentication {arrow}", + + "displayNameHeader": "Display Name", + "usernameHeader": "Username", + "emailHeader": "Email", + "adminHeader": "Admin?", + "authoptionsHeader": "Auth Options", + "srEditLabel": "Edit", + + "adminUserLabel": "Admin user", + "normalUserLabel": "Normal user", + + "authentication": { + "title": "Authentication", + "description": "Drop supports a variety of \"authentication mechanisms\". As you enable or disable them, they are shown on the sign in screen for users to select from. Click the dot menu to configure the authentication mechanism.", + "enabledKey": "Enabled?", + "enabled": "Enabled", + "disabled": "Disabled", + "srOpenOptions": "Open options", + "configure": "Configure", + + "simple": "Simple (username/password)", + "oidc": "OpenID Connect" + }, + + "simple": { + "title": "Simple authentication", + "description": "Simple authentication uses a system of 'invitations' to create users. You can create an invitation, and optionally specify a username or email for the user, and then it will generate a magic URL that can be used to create an account.", + + "invitationTitle": "invitations", + "createInvitation": "Create invitation", + + "noUsernameEnforced": "No username enforced.", + "noEmailEnforced": "No email enforced.", + + "adminInvitation": "Admin invitation", + "userInvitation": "User invitation", + + "expires": "Expires: {expiry}", + "neverExpires": "Never expires.", + + "noInvitations": "No invitations.", + + "inviteTitle": "Invite user to Drop", + "inviteDescription": "Drop will generate a URL that you can send to the person you want to invite. You can optionally specify a username or email for them to use.", + + "inviteUsernameLabel": "Username (optional)", + "inviteUsernameFormat": "Must be 5 or more characters", + "inviteUsernamePlaceholder": "myUsername", + + "inviteEmailLabel": "Email address (optional)", + "inviteEmailDescription": "Must be in the format user{'@'}example.com", + "inviteEmailPlaceholder": "me{'@'}example.com", + + "inviteAdminSwitchLabel": "Admin invitation", + "inviteAdminSwitchDescription": "Create this user as an administrator", + + "inviteExpiryLabel": "Expires", + + "inviteButton": "Invite", + + "invite3Days": "3 days", + "inviteWeek": "1 week", + "inviteMonth": "1 month", + "invite6Months": "6 months", + "inviteYear": "1 year", + "inviteNever": "Never" + } + } + }, "library": { "addGames": "All Games", "addToLib": "Add to Library", @@ -228,13 +334,36 @@ }, "metadataProvider": "Metadata provider", "noGames": "No games imported", - "noVersions": "You have no versions of this game available.", - "noVersionsAdded": "no versions added", - "openInMetadata": "Open in Metadata", - "openLibrary": "Open with Library {arrow}", - "openMetadata": "Open with Metadata {arrow}", + "openEditor": "Open in Editor {arrow}", "openStore": "Open in Store", "shortDesc": "Short Description", + "version": { + "noVersions": "You have no versions of this game available.", + "noVersionsAdded": "no versions added", + "delta": "Upgrade mode" + }, + "game": { + "imageCarousel": "Image Carousel", + "imageCarouselDescription": "Customise what images and what order are shown on the store page.", + "addImageCarousel": "Add from image library", + "imageCarouselEmpty": "No images added to the carousel yet.", + "removeImageCarousel": "Remove image", + "addCarouselNoImages": "No images to add.", + + "imageLibrary": "Image library", + "imageLibraryDescription": "Please note all images uploaded are accessible to all users through browser dev-tools.", + "setBanner": "Set as banner", + "setCover": "Set as cover", + "deleteImage": "Delete image", + + "currentBanner": "banner", + "currentCover": "cover", + + "addDescriptionNoImages": "No images to add.", + + "editGameName": "Game Name", + "editGameDescription": "Game Description" + }, "sources": { "create": "Create source", "createDesc": "Drop will use this source to access your game library, and make them available.", @@ -310,6 +439,8 @@ }, "options": "Options", "save": "Save", + "add": "Add", + "insert": "Insert", "security": "Security", "settings": "Settings", "store": { @@ -347,5 +478,10 @@ "settings": "Account settings" } }, + "task": { + "successful": "Successful!", + "successfulDescription": "\"{0}\" completed successfully" + }, + "todo": "Todo", "welcome": "American, Welcome!" } diff --git a/layouts/admin.vue b/layouts/admin.vue index 6c7ba76..04bb205 100644 --- a/layouts/admin.vue +++ b/layouts/admin.vue @@ -162,7 +162,6 @@ import { ServerStackIcon, HomeIcon, Cog6ToothIcon, - DocumentIcon, UserGroupIcon, RectangleStackIcon, } from "@heroicons/vue/24/outline"; @@ -181,12 +180,6 @@ const navigation: Array = [ prefix: "/admin/library", icon: ServerStackIcon, }, - { - label: t("header.admin.meta"), - route: "/admin/metadata", - prefix: "/admin/metadata", - icon: DocumentIcon, - }, { label: t("header.admin.users"), route: "/admin/users", diff --git a/layouts/default.vue b/layouts/default.vue index 11303ea..40b9307 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -15,14 +15,15 @@ const route = useRoute(); const noWrapper = !!route.query.noWrapper; +const { t } = useI18n(); + useHead({ htmlAttrs: { lang: "en", }, link: [], titleTemplate(title) { - if (title) return `${title} | Drop`; - return `Drop`; + return title ? t("titleTemplate", [title]) : t("title"); }, }); diff --git a/package.json b/package.json index cea0cb4..abf97f8 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "autoprefixer": "^10.4.20", "eslint": "^9.24.0", "eslint-config-prettier": "^10.1.1", - "h3": "^1.15.1", + "h3": "^1.15.3", + "nitropack": "^2.11.12", "ofetch": "^1.4.1", "postcss": "^8.4.47", "prettier": "^3.5.3", diff --git a/pages/account/index.vue b/pages/account/index.vue index 2358092..38070ff 100644 --- a/pages/account/index.vue +++ b/pages/account/index.vue @@ -1,4 +1,8 @@ diff --git a/pages/admin/index.vue b/pages/admin/index.vue index 60dac71..feb6cd0 100644 --- a/pages/admin/index.vue +++ b/pages/admin/index.vue @@ -1,170 +1,6 @@ - + diff --git a/pages/admin/library/[id]/index.vue b/pages/admin/library/[id]/index.vue index 6e94827..4dd18a1 100644 --- a/pages/admin/library/[id]/index.vue +++ b/pages/admin/library/[id]/index.vue @@ -1,163 +1,132 @@ - diff --git a/pages/admin/library/index.vue b/pages/admin/library/index.vue index 82e8a29..2bf3346 100644 --- a/pages/admin/library/index.vue +++ b/pages/admin/library/index.vue @@ -98,19 +98,13 @@
- - - - - - + @@ -169,7 +163,7 @@

- {{ $t("library.admin.noVersions") }} + {{ $t("library.admin.version.noVersions") }}

diff --git a/pages/admin/metadata/games/index.vue b/pages/admin/metadata/games/index.vue deleted file mode 100644 index db54e43..0000000 --- a/pages/admin/metadata/games/index.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - diff --git a/pages/admin/metadata/index.vue b/pages/admin/metadata/index.vue deleted file mode 100644 index 1465bda..0000000 --- a/pages/admin/metadata/index.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/pages/admin/settings/index.vue b/pages/admin/settings/index.vue index 3a7c59e..8366f2f 100644 --- a/pages/admin/settings/index.vue +++ b/pages/admin/settings/index.vue @@ -1,5 +1,5 @@