mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
i18n Support and Task improvements (#80)
* fix: release workflow * feat: move mostly to internal tasks system * feat: migrate object clean to new task system * fix: release not getting good base version * chore: set version v0.3.0 * chore: style * feat: basic task concurrency * feat: temp pages to fill in page links * feat: inital i18n support * feat: localize store page * chore: style * fix: weblate doesn't like multifile thing * fix: update nuxt * feat: improved error logging * fix: using old task api * feat: basic translation docs * feat: add i18n eslint plugin * feat: translate store and auth pages * feat: more translation progress * feat: admin dash i18n progress * feat: enable update check by default in prod * fix: using wrong i18n keys * fix: crash in library sources page * feat: finish i18n work * fix: missing i18n translations * feat: use twemoji for emojis * feat: sanatize object ids * fix: EmojiText's alt text * fix: UserWidget not using links * feat: cache and auth for emoji api * fix: add more missing translations
This commit is contained in:
+7
-4
@@ -42,7 +42,9 @@
|
||||
class="-m-2.5 p-2.5"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<span class="sr-only">{{
|
||||
$t("userHeader.closeSidebar")
|
||||
}}</span>
|
||||
<XMarkIcon class="size-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -73,13 +75,13 @@
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<Bars3Icon class="size-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div
|
||||
class="flex-1 text-sm/6 font-semibold uppercase font-display text-zinc-400"
|
||||
>
|
||||
Account
|
||||
{{ $t("account.title") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,6 +102,7 @@ import {
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
router.afterEach(() => {
|
||||
@@ -107,6 +110,6 @@ router.afterEach(() => {
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Account",
|
||||
title: t("account.title"),
|
||||
});
|
||||
</script>
|
||||
|
||||
+25
-13
@@ -4,12 +4,12 @@
|
||||
<h2
|
||||
class="mt-2 text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||
>
|
||||
Devices
|
||||
{{ $t("account.devices.title") }}
|
||||
</h2>
|
||||
<p
|
||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||
>
|
||||
Manage the devices authorized to access your Drop account.
|
||||
{{ $t("account.devices.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -24,28 +24,28 @@
|
||||
scope="col"
|
||||
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-6"
|
||||
>
|
||||
Name
|
||||
{{ $t("name") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Platform
|
||||
{{ $t("account.devices.platform") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Capabilities
|
||||
{{ $t("account.devices.capabilities") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Last Connected
|
||||
{{ $t("account.devices.lastConnected") }}
|
||||
</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Actions</span>
|
||||
<span class="sr-only">{{ $t("actions") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -80,7 +80,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
{{ DateTime.fromISO(client.lastConnected).toRelative() }}
|
||||
<RelativeTime :date="client.lastConnected" />
|
||||
</td>
|
||||
<td
|
||||
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
|
||||
@@ -89,13 +89,16 @@
|
||||
class="inline-flex items-center rounded-md bg-red-400/10 px-2 py-1 text-xs font-medium text-red-400 ring-1 ring-inset ring-red-400/20 transition-all duration-200 hover:bg-red-400/20 hover:scale-105 active:scale-95"
|
||||
@click="() => revokeClientWrapper(client.id)"
|
||||
>
|
||||
Revoke<span class="sr-only">, {{ client.name }}</span>
|
||||
{{ $t("account.devices.revoke") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [client.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clients.length === 0">
|
||||
<td colspan="5" class="py-8 text-center text-sm text-zinc-400">
|
||||
No devices connected to your account.
|
||||
{{ $t("account.devices.noDevices") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -107,16 +110,25 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon } from "@heroicons/vue/24/outline";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore pending https://github.com/nitrojs/nitro/issues/2758
|
||||
const clients = ref(await $dropFetch("/api/v1/user/client"));
|
||||
const { t } = useI18n();
|
||||
|
||||
async function revokeClient(id: string) {
|
||||
await $dropFetch(`/api/v1/user/client/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// clients.value.push({
|
||||
// id: "example-client",
|
||||
// userId: "example-user",
|
||||
// name: "Example Client",
|
||||
// platform: "Windows",
|
||||
// capabilities: ["TrackPlaytime"],
|
||||
// lastConnected: new Date().toISOString(),
|
||||
// });
|
||||
|
||||
function revokeClientWrapper(id: string) {
|
||||
revokeClient(id)
|
||||
.then(() => {
|
||||
@@ -127,8 +139,8 @@ function revokeClientWrapper(id: string) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to revoke client",
|
||||
description: `Failed to revoke client: ${e}`,
|
||||
title: t("errors.revokeClient"),
|
||||
description: t("errors.revokeClientFull", String(e)),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||
>
|
||||
Notifications
|
||||
{{ $t("account.notifications.notifications") }}
|
||||
</h2>
|
||||
<button
|
||||
:disabled="notifications.length === 0"
|
||||
@@ -13,13 +13,13 @@
|
||||
@click="markAllAsRead"
|
||||
>
|
||||
<CheckIcon class="size-4" />
|
||||
Mark all as read
|
||||
{{ $t("account.notifications.markAllAsRead") }}
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||
>
|
||||
View and manage your notifications.
|
||||
{{ $t("account.notifications.desc") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
<div class="ml-4 flex flex-shrink-0 items-center gap-x-2">
|
||||
<span class="text-xs text-zinc-500">
|
||||
{{ DateTime.fromISO(notification.created).toRelative() }}
|
||||
<RelativeTime :date="notification.created" />
|
||||
</span>
|
||||
<button
|
||||
v-if="!notification.read"
|
||||
@@ -61,7 +61,7 @@
|
||||
@click="markAsRead(notification.id)"
|
||||
>
|
||||
<CheckIcon class="size-3" />
|
||||
Mark as read
|
||||
{{ $t("account.notifications.markAsRead") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -69,7 +69,7 @@
|
||||
@click="deleteNotification(notification.id)"
|
||||
>
|
||||
<TrashIcon class="size-3" />
|
||||
Delete
|
||||
{{ $t("delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +80,9 @@
|
||||
v-if="notifications.length === 0"
|
||||
class="rounded-xl border border-zinc-800 bg-zinc-900 p-8 text-center"
|
||||
>
|
||||
<p class="text-sm text-zinc-400">No notifications</p>
|
||||
<p class="text-sm text-zinc-400">
|
||||
{{ $t("account.notifications.none") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +90,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { DateTime } from "luxon";
|
||||
import type { Notification } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
|
||||
@@ -96,8 +97,10 @@ definePageMeta({
|
||||
layout: "default",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
useHead({
|
||||
title: "Notifications",
|
||||
title: t("account.notifications.title"),
|
||||
});
|
||||
|
||||
// Fetch notifications
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
:model-value="currentlySelectedVersion"
|
||||
@update:model-value="(value) => updateCurrentlySelectedVersion(value)"
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Select version to import</ListboxLabel
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">{{
|
||||
$t("library.admin.import.version.version")
|
||||
}}</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
@@ -15,9 +15,9 @@
|
||||
<span v-if="currentlySelectedVersion != -1" class="block truncate">{{
|
||||
versions[currentlySelectedVersion]
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-600"
|
||||
>Please select a directory...</span
|
||||
>
|
||||
<span v-else class="block truncate text-zinc-600">{{
|
||||
$t("library.admin.import.selectDir")
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
@@ -79,17 +79,20 @@
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Setup executable/command</label
|
||||
>{{ $t("library.admin.import.version.setupCmd") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 text-xs">Ran once when the game is installed</p>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.setupDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<span
|
||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||
>(install_dir)/</span
|
||||
>
|
||||
{{ $t("library.admin.import.version.installDir") }}
|
||||
</span>
|
||||
<Combobox
|
||||
as="div"
|
||||
:value="versionSettings.setup"
|
||||
@@ -99,7 +102,9 @@
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
:placeholder="'setup.exe'"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.setupPlaceholder')
|
||||
"
|
||||
@change="setupProcessQuery = $event.target.value"
|
||||
@blur="setupProcessQuery = ''"
|
||||
/>
|
||||
@@ -171,7 +176,7 @@
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
"{{ setupProcessQuery }}"
|
||||
{{ $t("chars.quoted", { text: setupProcessQuery }) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
@@ -206,14 +211,11 @@
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Setup mode</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>When enabled, this version does not have a launch command, and
|
||||
simply runs the executable on the user's computer. Useful for games
|
||||
that only distribute installer and not portable
|
||||
files.</SwitchDescription
|
||||
>{{ $t("library.admin.import.version.setupMode") }}</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">{{
|
||||
$t("library.admin.import.version.setupModeDesc")
|
||||
}}</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="versionSettings.onlySetup"
|
||||
@@ -235,16 +237,18 @@
|
||||
<label
|
||||
for="startup"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Launch executable/command</label
|
||||
>{{ $t("library.admin.import.version.launchCmd") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 text-xs">Executable to launch the game</p>
|
||||
<p class="text-zinc-400 text-xs">
|
||||
{{ $t("library.admin.import.version.launchDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="flex w-fit rounded-md shadow-sm bg-zinc-950 ring-1 ring-inset ring-zinc-800 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600"
|
||||
>
|
||||
<span
|
||||
class="flex select-none items-center pl-3 text-zinc-500 sm:text-sm"
|
||||
>(install_dir)/</span
|
||||
>{{ $t("library.admin.import.version.installDir") }}</span
|
||||
>
|
||||
<Combobox
|
||||
as="div"
|
||||
@@ -255,7 +259,9 @@
|
||||
<div class="relative">
|
||||
<ComboboxInput
|
||||
class="block flex-1 border-0 py-1.5 pl-1 bg-transparent text-zinc-100 placeholder:text-zinc-400 focus:ring-0 sm:text-sm sm:leading-6"
|
||||
:placeholder="'game.exe'"
|
||||
:placeholder="
|
||||
$t('library.admin.import.version.launchPlaceholder')
|
||||
"
|
||||
@change="launchProcessQuery = $event.target.value"
|
||||
@blur="launchProcessQuery = ''"
|
||||
/>
|
||||
@@ -327,7 +333,7 @@
|
||||
<span
|
||||
:class="['block truncate', selected && 'font-semibold']"
|
||||
>
|
||||
"{{ launchProcessQuery }}"
|
||||
{{ $t("chars.quoted", { text: launchProcessQuery }) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
@@ -361,7 +367,7 @@
|
||||
</div>
|
||||
|
||||
<PlatformSelector v-model="versionSettings.platform">
|
||||
Version platform
|
||||
{{ $t("library.admin.import.version.platform") }}
|
||||
</PlatformSelector>
|
||||
<SwitchGroup as="div" class="flex items-center justify-between">
|
||||
<span class="flex flex-grow flex-col">
|
||||
@@ -369,13 +375,12 @@
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Update mode</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>When enabled, these files will be installed on top of (overwriting)
|
||||
the previous version's. If multiple "update modes" are chained
|
||||
together, they are applied in order.</SwitchDescription
|
||||
>
|
||||
{{ $t("library.admin.import.version.updateMode") }}
|
||||
</SwitchLabel>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">
|
||||
{{ $t("library.admin.import.version.updateModeDesc") }}
|
||||
</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="versionSettings.delta"
|
||||
@@ -398,7 +403,9 @@
|
||||
<DisclosureButton
|
||||
class="border-b border-zinc-600 pb-2 flex w-full items-start justify-between text-left text-zinc-100"
|
||||
>
|
||||
<span class="text-base/7 font-semibold">Advanced options</span>
|
||||
<span class="text-base/7 font-semibold">
|
||||
{{ $t("library.admin.import.version.advancedOptions") }}
|
||||
</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<ChevronUpIcon v-if="!open" class="size-6" aria-hidden="true" />
|
||||
<ChevronDownIcon v-else class="size-6" aria-hidden="true" />
|
||||
@@ -420,13 +427,12 @@
|
||||
as="span"
|
||||
class="text-sm font-medium leading-6 text-zinc-100"
|
||||
passive
|
||||
>Override UMU Launcher Game ID</SwitchLabel
|
||||
>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400"
|
||||
>By default, Drop uses a non-ID when launching with UMU
|
||||
Launcher. In order to get the right patches for some games,
|
||||
you may have to manually set this field.</SwitchDescription
|
||||
>
|
||||
{{ $t("library.admin.import.version.umuOverride") }}
|
||||
</SwitchLabel>
|
||||
<SwitchDescription as="span" class="text-sm text-zinc-400">
|
||||
{{ $t("library.admin.import.version.umuOverrideDesc") }}
|
||||
</SwitchDescription>
|
||||
</span>
|
||||
<Switch
|
||||
v-model="umuIdEnabled"
|
||||
@@ -448,8 +454,9 @@
|
||||
<label
|
||||
for="umu-id"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>UMU Launcher ID</label
|
||||
>
|
||||
{{ $t("library.admin.import.version.umuLauncherId") }}
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
id="umu-id"
|
||||
@@ -467,7 +474,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="text-zinc-400">
|
||||
No advanced options for this configuration.
|
||||
{{ $t("library.admin.import.version.noAdv") }}
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
@@ -477,7 +484,7 @@
|
||||
:loading="importLoading"
|
||||
@click="startImport_wrapper"
|
||||
>
|
||||
Import
|
||||
{{ $t("library.admin.import.import") }}
|
||||
</LoadingButton>
|
||||
<div v-if="importError" class="mt-4 w-fit rounded-md bg-red-600/10 p-4">
|
||||
<div class="flex">
|
||||
@@ -497,7 +504,7 @@
|
||||
role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
|
||||
>
|
||||
Loading version metadata...
|
||||
{{ $t("library.admin.import.version.loadingVersion") }}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-white"
|
||||
@@ -514,7 +521,6 @@
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -548,7 +554,7 @@ definePageMeta({
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const gameId = route.params.id.toString();
|
||||
const versions = await $dropFetch(
|
||||
@@ -661,7 +667,7 @@ function startImport_wrapper() {
|
||||
importLoading.value = true;
|
||||
startImport()
|
||||
.catch((error) => {
|
||||
importError.value = error.statusMessage ?? "An unknown error occurred.";
|
||||
importError.value = error.statusMessage ?? t("errors.unknown");
|
||||
})
|
||||
.finally(() => {
|
||||
importLoading.value = false;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
type="button"
|
||||
class="inline-flex w-fit items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Open in Metadata
|
||||
{{ $t("library.admin.openInMetadata") }}
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="-mr-0.5 h-7 w-7 p-1"
|
||||
aria-hidden="true"
|
||||
@@ -40,7 +40,7 @@
|
||||
type="button"
|
||||
class="inline-flex w-fit items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Open in Store
|
||||
{{ $t("library.admin.openStore") }}
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="-mr-0.5 h-7 w-7 p-1"
|
||||
aria-hidden="true"
|
||||
@@ -59,7 +59,7 @@
|
||||
<h3
|
||||
class="text-base font-semibold font-display leading-6 text-zinc-100"
|
||||
>
|
||||
Version priority
|
||||
{{ $t("library.admin.versionPriority") }}
|
||||
|
||||
<!-- import games button -->
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
>
|
||||
{{
|
||||
unimportedVersions.length > 0
|
||||
? "Import version"
|
||||
: "No versions to import"
|
||||
? $t("library.admin.import.version.import")
|
||||
: $t("library.admin.import.version.noVersions")
|
||||
}}
|
||||
</NuxtLink>
|
||||
</h3>
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center w-full text-sm text-zinc-600">
|
||||
lowest
|
||||
{{ $t("lowest") }}
|
||||
</div>
|
||||
<draggable
|
||||
:list="game.versions"
|
||||
@@ -105,7 +105,11 @@
|
||||
{{ item.versionName }}
|
||||
</div>
|
||||
<div class="text-zinc-400">
|
||||
{{ item.delta ? "Upgrade mode" : "" }}
|
||||
{{
|
||||
item.delta
|
||||
? $t("library.admin.import.version.updateMode")
|
||||
: ""
|
||||
}}
|
||||
</div>
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
<component
|
||||
@@ -126,10 +130,10 @@
|
||||
v-if="game.versions.length == 0"
|
||||
class="text-center font-bold text-zinc-400 my-3"
|
||||
>
|
||||
no versions added
|
||||
{{ $t("library.admin.noVersionsAdded") }}
|
||||
</div>
|
||||
<div class="mt-2 text-center w-full text-sm text-zinc-600">
|
||||
highest
|
||||
{{ $t("highest") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,6 +155,8 @@ definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// TODO implement UI for this
|
||||
|
||||
const route = useRoute();
|
||||
@@ -174,12 +180,12 @@ async function updateVersionOrder() {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "There an error while updating the version order",
|
||||
description: `Drop encountered an error while updating the version: ${
|
||||
title: t("errors.version.order.title"),
|
||||
description: t("errors.version.order.desc", {
|
||||
// @ts-expect-error attempt to get statusMessage on error
|
||||
e?.statusMessage ?? "An unknown error occurred"
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
error: e?.statusMessage ?? t("errors.unknown"),
|
||||
}),
|
||||
buttonText: t("close"),
|
||||
},
|
||||
(e, c) => c(),
|
||||
);
|
||||
@@ -203,12 +209,12 @@ async function deleteVersion(versionName: string) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "There an error while deleting the version",
|
||||
description: `Drop encountered an error while deleting the version: ${
|
||||
title: t("errors.version.delete.title"),
|
||||
description: t("errors.version.delete.desc", {
|
||||
// @ts-expect-error attempt to get statusMessage on error
|
||||
e?.statusMessage ?? "An unknown error occurred"
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
error: e?.statusMessage ?? t("errors.unknown"),
|
||||
}),
|
||||
buttonText: t("close"),
|
||||
},
|
||||
(e, c) => c(),
|
||||
);
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
:model="currentlySelectedGame"
|
||||
@update:model-value="(value) => updateSelectedGame_wrapper(value)"
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Select game to import</ListboxLabel
|
||||
>
|
||||
<ListboxLabel class="block text-sm font-medium leading-6 text-zinc-100">
|
||||
{{ $t("library.admin.import.selectGame") }}
|
||||
</ListboxLabel>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
@@ -15,9 +15,9 @@
|
||||
<span v-if="currentlySelectedGame != -1" class="block truncate">{{
|
||||
games.unimportedGames[currentlySelectedGame].game
|
||||
}}</span>
|
||||
<span v-else class="block truncate text-zinc-400"
|
||||
>Please select a directory...</span
|
||||
>
|
||||
<span v-else class="block truncate text-zinc-400">{{
|
||||
$t("library.admin.import.selectDir")
|
||||
}}</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
@@ -80,7 +80,8 @@
|
||||
class="w-fit"
|
||||
:loading="importLoading"
|
||||
@click="() => importGame_wrapper(false)"
|
||||
>Import without metadata
|
||||
>
|
||||
{{ $t("library.admin.import.withoutMetadata") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +90,7 @@
|
||||
class="inline-flex items-center gap-x-4 text-zinc-600 font-display font-bold"
|
||||
>
|
||||
<div class="h-[1px] grow bg-zinc-800" />
|
||||
OR
|
||||
{{ $t("auth.signin.or") }}
|
||||
<div class="h-[1px] grow bg-zinc-800" />
|
||||
</div>
|
||||
|
||||
@@ -100,7 +101,7 @@
|
||||
<label
|
||||
for="searchTerm"
|
||||
class="block text-sm/6 font-medium text-zinc-100"
|
||||
>Search</label
|
||||
>{{ $t("library.admin.import.search") }}</label
|
||||
>
|
||||
<div class="mt-2 flex">
|
||||
<div class="-mr-px grid grow grid-cols-1 focus-within:relative">
|
||||
@@ -110,7 +111,7 @@
|
||||
type="text"
|
||||
name="searchTerm"
|
||||
class="col-start-1 row-start-1 block w-full rounded-l-md bg-zinc-950 py-1.5 px-3 text-base text-zinc-100 outline-1 -outline-offset-1 outline-zinc-800 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
|
||||
placeholder="John Smith"
|
||||
:placeholder="$t('library.admin.import.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<LoadingButton
|
||||
@@ -123,7 +124,7 @@
|
||||
class="-ml-0.5 size-4 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Search
|
||||
{{ $t("library.admin.import.search") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
@@ -135,7 +136,7 @@
|
||||
>
|
||||
<ListboxLabel
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Select game</ListboxLabel
|
||||
>{{ $t("library.admin.import.selectGameSearch") }}</ListboxLabel
|
||||
>
|
||||
<div class="relative mt-2">
|
||||
<ListboxButton
|
||||
@@ -145,9 +146,9 @@
|
||||
v-if="currentlySelectedMetadata != -1"
|
||||
:game="metadataResults[currentlySelectedMetadata]"
|
||||
/>
|
||||
<span v-else class="block truncate text-zinc-600"
|
||||
>Please select a game...</span
|
||||
>
|
||||
<span v-else class="block truncate text-zinc-600">
|
||||
{{ $t("library.admin.import.selectGamePlaceholder") }}
|
||||
</span>
|
||||
<span
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
>
|
||||
@@ -191,7 +192,7 @@
|
||||
role="status"
|
||||
class="inline-flex text-zinc-100 font-display font-semibold items-center gap-x-4"
|
||||
>
|
||||
Loading game results...
|
||||
{{ $t("library.admin.import.loading") }}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-6 h-6 text-transparent animate-spin fill-white"
|
||||
@@ -208,7 +209,6 @@
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -233,7 +233,8 @@
|
||||
:loading="importLoading"
|
||||
:disabled="currentlySelectedMetadata === -1"
|
||||
@click="() => importGame_wrapper()"
|
||||
>Import
|
||||
>
|
||||
{{ $t("library.admin.import.import") }}
|
||||
</LoadingButton>
|
||||
|
||||
<div
|
||||
@@ -274,6 +275,8 @@ definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const games = await $dropFetch("/api/v1/admin/import/game");
|
||||
const currentlySelectedGame = ref(-1);
|
||||
const gameSearchResultsLoading = ref(false);
|
||||
@@ -308,8 +311,7 @@ function updateSelectedGame_wrapper(value: number) {
|
||||
gameSearchResultsLoading.value = true;
|
||||
updateSelectedGame(value)
|
||||
.catch((error) => {
|
||||
gameSearchResultsError.value =
|
||||
error.statusMessage || "An unknown error occurred";
|
||||
gameSearchResultsError.value = error.statusMessage || t("errors.unknown");
|
||||
})
|
||||
.finally(() => {
|
||||
gameSearchResultsLoading.value = false;
|
||||
@@ -348,7 +350,7 @@ function importGame_wrapper(metadata = true) {
|
||||
importError.value = undefined;
|
||||
importGame(metadata)
|
||||
.catch((error) => {
|
||||
importError.value = error?.statusMessage || "An unknown error occurred.";
|
||||
importError.value = error?.statusMessage || t("errors.unknown");
|
||||
})
|
||||
.finally(() => {
|
||||
importLoading.value = false;
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="space-y-4">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-zinc-100">Game Library</h1>
|
||||
<h1 class="text-base font-semibold text-zinc-100">
|
||||
{{ $t("library.admin.gameLibrary") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
As you add folders to your library sources, Drop will detect it and
|
||||
prompt you to import it. Each game needs to be imported before you can
|
||||
import a version.
|
||||
{{ $t("library.admin.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
@@ -14,7 +14,11 @@
|
||||
to="/admin/library/sources"
|
||||
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 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"
|
||||
>
|
||||
Sources →
|
||||
<i18n-t keypath="library.admin.sources.link" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,15 +32,18 @@
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p class="text-sm text-blue-400">
|
||||
Drop has detected you have new games to import.
|
||||
{{ $t("library.admin.detectedGame") }}
|
||||
</p>
|
||||
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
<NuxtLink
|
||||
href="/admin/library/import"
|
||||
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
||||
>
|
||||
Import
|
||||
<span aria-hidden="true"> →</span>
|
||||
<i18n-t keypath="library.admin.import.link" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,7 +56,7 @@
|
||||
type="text"
|
||||
name="search"
|
||||
class="col-start-1 row-start-1 block w-full rounded-md bg-zinc-900 py-1.5 pl-10 pr-3 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:pl-9 sm:text-sm/6"
|
||||
placeholder="Search library..."
|
||||
:placeholder="$t('library.search')"
|
||||
/>
|
||||
<MagnifyingGlassIcon
|
||||
class="pointer-events-none col-start-1 row-start-1 ml-3 size-5 self-center text-zinc-400 sm:size-4"
|
||||
@@ -80,30 +87,40 @@
|
||||
>
|
||||
</h3>
|
||||
<dl class="mt-1 flex flex-col justify-between">
|
||||
<dt class="sr-only">Short Description</dt>
|
||||
<dt class="sr-only">{{ $t("library.admin.shortDesc") }}</dt>
|
||||
<dd class="text-sm text-zinc-400">
|
||||
{{ game.mShortDescription }}
|
||||
</dd>
|
||||
<dt class="sr-only">Metadata provider</dt>
|
||||
<dt class="sr-only">
|
||||
{{ $t("library.admin.metadataProvider") }}
|
||||
</dt>
|
||||
</dl>
|
||||
<div class="mt-4 flex flex-col gap-y-1">
|
||||
<NuxtLink
|
||||
:href="`/admin/library/${game.id}`"
|
||||
class="w-fit rounded-md bg-blue-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 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"
|
||||
>
|
||||
Open with Library →
|
||||
<i18n-t keypath="library.admin.openLibrary" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
:href="`/admin/metadata/games/${game.id}`"
|
||||
class="w-fit rounded-md bg-zinc-800 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
|
||||
>
|
||||
Open with Metadata →
|
||||
<i18n-t keypath="library.admin.openMetadata" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<button
|
||||
class="w-fit rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-red-500 hover:scale-105 hover:shadow-lg hover:shadow-red-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
@click="() => deleteGame(game.id)"
|
||||
>
|
||||
Delete
|
||||
{{ $t("delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,15 +139,18 @@
|
||||
</div>
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p class="text-sm text-blue-400">
|
||||
Drop has detected you have new verions of this game to import.
|
||||
{{ $t("library.admin.detectedVersion") }}
|
||||
</p>
|
||||
<p class="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
<NuxtLink
|
||||
:href="`/admin/library/${game.id}/import`"
|
||||
class="whitespace-nowrap font-medium text-blue-400 hover:text-blue-500"
|
||||
>
|
||||
Import
|
||||
<span aria-hidden="true"> →</span>
|
||||
<i18n-t keypath="library.admin.import.link" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
@@ -149,7 +169,7 @@
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-600">
|
||||
You have no versions of this game available.
|
||||
{{ $t("library.admin.noVersions") }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,13 +180,13 @@
|
||||
v-if="filteredLibraryGames.length == 0 && libraryGames.length != 0"
|
||||
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
|
||||
>
|
||||
No results
|
||||
{{ $t("common.noResults") }}
|
||||
</p>
|
||||
<p
|
||||
v-if="filteredLibraryGames.length == 0 && libraryGames.length == 0"
|
||||
class="text-zinc-600 text-sm font-display font-bold uppercase text-center col-span-4"
|
||||
>
|
||||
No games imported
|
||||
{{ $t("library.admin.noGames") }}
|
||||
</p>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -176,12 +196,15 @@
|
||||
import { ExclamationTriangleIcon } from "@heroicons/vue/16/solid";
|
||||
import { InformationCircleIcon } from "@heroicons/vue/20/solid";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Libraries",
|
||||
title: t("library.admin.title"),
|
||||
});
|
||||
|
||||
const searchQuery = ref("");
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<div>
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-zinc-100">Library Sources</h1>
|
||||
<h1 class="text-base font-semibold text-zinc-100">
|
||||
{{ $t("library.admin.sources.sources") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
Configure your library sources, where Drop will look for new games and
|
||||
versions to import.
|
||||
{{ $t("library.admin.sources.desc") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
@@ -13,7 +14,7 @@
|
||||
class="block rounded-md bg-blue-600 px-3 py-2 text-center 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"
|
||||
@click="() => (actionSourceOpen = true)"
|
||||
>
|
||||
Create
|
||||
{{ $t("create") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,28 +28,28 @@
|
||||
scope="col"
|
||||
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-3"
|
||||
>
|
||||
Name
|
||||
{{ $t("name") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Type
|
||||
{{ $t("type") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Working?
|
||||
{{ $t("library.admin.sources.working") }}
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
|
||||
>
|
||||
Options
|
||||
{{ $t("options") }}
|
||||
</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-3">
|
||||
<span class="sr-only">Edit</span>
|
||||
<span class="sr-only">{{ $t("edit") }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -83,14 +84,20 @@
|
||||
class="text-blue-500 hover:text-blue-400"
|
||||
@click="() => edit(sourceIdx)"
|
||||
>
|
||||
Edit<span class="sr-only">, {{ source.name }}</span>
|
||||
{{ $t("edit") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [source.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-red-500 hover:text-red-400"
|
||||
@click="() => deleteSource(sourceIdx)"
|
||||
>
|
||||
Delete<span class="sr-only">, {{ source.name }}</span>
|
||||
{{ $t("delete") }}
|
||||
<span class="sr-only">
|
||||
{{ $t("chars.srComma", [source.name]) }}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,11 +111,10 @@
|
||||
<template #default>
|
||||
<div>
|
||||
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-white">
|
||||
Create source
|
||||
{{ $t("library.admin.sources.create") }}
|
||||
</DialogTitle>
|
||||
<p class="mt-1 text-zinc-400 text-sm">
|
||||
Drop will use this source to access your game library, and make them
|
||||
available.
|
||||
{{ $t("library.admin.sources.createDesc") }}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
@@ -119,10 +125,10 @@
|
||||
<label
|
||||
for="name"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Name</label
|
||||
>{{ $t("name") }}</label
|
||||
>
|
||||
<p class="text-zinc-400 block text-xs font-medium leading-6">
|
||||
The name of your source, for reference.
|
||||
{{ $t("library.admin.sources.nameDesc") }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<input
|
||||
@@ -131,21 +137,23 @@
|
||||
name="name"
|
||||
type="text"
|
||||
autocomplete="name"
|
||||
placeholder="My New Source"
|
||||
:placeholder="$t('library.admin.sources.namePlaceholder')"
|
||||
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="createMode">
|
||||
<label class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Type</label
|
||||
>
|
||||
<label class="block text-sm font-medium leading-6 text-zinc-100">{{
|
||||
$t("type")
|
||||
}}</label>
|
||||
<p class="text-zinc-400 block text-xs font-medium leading-6">
|
||||
The type of your source. Changes the required options.
|
||||
{{ $t("library.admin.sources.typeDesc") }}
|
||||
</p>
|
||||
|
||||
<RadioGroup v-model="currentSourceOption" class="mt-2">
|
||||
<RadioGroupLabel class="sr-only">Type</RadioGroupLabel>
|
||||
<RadioGroupLabel class="sr-only">{{
|
||||
$t("type")
|
||||
}}</RadioGroupLabel>
|
||||
<div class="space-y-4">
|
||||
<RadioGroupOption
|
||||
v-for="[source, metadata] in optionsMetadataIter"
|
||||
@@ -220,7 +228,7 @@
|
||||
class="w-full sm:w-fit"
|
||||
@click="() => performActionSource_wrapper()"
|
||||
>
|
||||
{{ createMode ? "Create" : "Save" }}
|
||||
{{ createMode ? $t("create") : $t("save") }}
|
||||
</LoadingButton>
|
||||
<button
|
||||
ref="cancelButtonRef"
|
||||
@@ -233,7 +241,7 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
Cancel
|
||||
{{ $t("cancel") }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalTemplate>
|
||||
@@ -268,6 +276,8 @@ definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const sources = ref(
|
||||
await $dropFetch<WorkingLibrarySource[]>("/api/v1/admin/library/sources"),
|
||||
);
|
||||
@@ -293,8 +303,7 @@ const optionsMetadata: {
|
||||
};
|
||||
} = {
|
||||
Filesystem: {
|
||||
description:
|
||||
"Imports games from a path on disk. Requires version-based folder structure, and supports archived games.",
|
||||
description: t("library.admin.sources.fsDesc"),
|
||||
icon: DocumentIcon,
|
||||
},
|
||||
};
|
||||
@@ -367,9 +376,11 @@ async function deleteSource(index: number) {
|
||||
createModal(
|
||||
ModalType.Notification,
|
||||
{
|
||||
title: "Failed to delete library source",
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
description: `Drop couldn't add delete this source: ${e?.statusMessage}`,
|
||||
title: t("errors.library.source.delete.title"),
|
||||
description: t("errors.library.source.delete.desc", [
|
||||
// @ts-expect-error attempt to display statusMessage on error
|
||||
e?.statusMessage ?? t("errors.unknown"),
|
||||
]),
|
||||
},
|
||||
(_, c) => c(),
|
||||
);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
class="relative inline-flex gap-x-3 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"
|
||||
@click="() => (showEditCoreMetadata = true)"
|
||||
>
|
||||
Edit <PencilIcon class="size-4" />
|
||||
{{ $t("edit") }} <PencilIcon class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -597,7 +597,7 @@ watch(descriptionHTML, (_v) => {
|
||||
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 ?? "An unknown error occurred."
|
||||
e?.statusMessage ?? t("errors.unknown")
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
@@ -642,7 +642,7 @@ async function updateBannerImage(id: string) {
|
||||
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 ?? "An unknown error occurred"
|
||||
e?.statusMessage ?? t("errors.unknown")
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
@@ -670,7 +670,7 @@ async function updateCoverImage(id: string) {
|
||||
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 ?? "An unknown error occurred"
|
||||
e?.statusMessage ?? t("errors.unknown")
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
@@ -700,7 +700,7 @@ async function deleteImage(id: string) {
|
||||
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 ?? "An unknown error occurred"
|
||||
e?.statusMessage ?? t("errors.unknown")
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
@@ -743,7 +743,7 @@ async function updateImageCarousel() {
|
||||
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 ?? "An unknown error occurred"
|
||||
e?.statusMessage ?? t("errors.unknown")
|
||||
}`,
|
||||
buttonText: "Close",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="text-gray-100">Todo page</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: "Settings",
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="text-gray-100">Todo page</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: "Tasks",
|
||||
});
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
});
|
||||
</script>
|
||||
@@ -2,7 +2,9 @@
|
||||
<div>
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-base font-semibold text-zinc-100">Users</h1>
|
||||
<h1 class="text-base font-semibold text-zinc-100">
|
||||
{{ $t("header.admin.users") }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-zinc-400">
|
||||
Manage the users on your Drop instance, and configure your
|
||||
authentication methods.
|
||||
|
||||
+17
-15
@@ -9,10 +9,10 @@
|
||||
<h2
|
||||
class="mt-4 text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
|
||||
>
|
||||
Create your Drop account
|
||||
{{ $t("auth.register.title") }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm leading-6 text-zinc-400">
|
||||
Fill in your details below to create your account.
|
||||
{{ $t("auth.register.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<label
|
||||
for="display-name"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Display Name</label
|
||||
>{{ $t("auth.displayName") }}</label
|
||||
>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
@@ -43,7 +43,7 @@
|
||||
<label
|
||||
for="email"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Email address</label
|
||||
>{{ $t("auth.email") }}</label
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
@@ -51,7 +51,7 @@
|
||||
'block text-xs font-medium leading-6',
|
||||
]"
|
||||
>
|
||||
Must be in the format user@example.com
|
||||
{{ $t("auth.register.emailFormat") }}
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
@@ -74,7 +74,7 @@
|
||||
<label
|
||||
for="username"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Username</label
|
||||
>{{ $t("auth.username") }}</label
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
@@ -82,7 +82,7 @@
|
||||
'block text-xs font-medium leading-6',
|
||||
]"
|
||||
>
|
||||
Must be 5 or more characters, and lowercase
|
||||
{{ $t("auth.register.usernameFormat") }}
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
@@ -105,7 +105,7 @@
|
||||
<label
|
||||
for="password"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Password</label
|
||||
>{{ $t("auth.password") }}</label
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
@@ -113,7 +113,7 @@
|
||||
'block text-xs font-medium leading-6',
|
||||
]"
|
||||
>
|
||||
Must be 14 or more characters
|
||||
{{ $t("auth.register.passwordFormat") }}
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
@@ -132,7 +132,7 @@
|
||||
<label
|
||||
for="confirm-password"
|
||||
class="block text-sm font-medium leading-6 text-zinc-100"
|
||||
>Confirm Password</label
|
||||
>{{ $t("auth.confirmPassword") }}</label
|
||||
>
|
||||
<p
|
||||
:class="[
|
||||
@@ -140,7 +140,7 @@
|
||||
'block text-xs font-medium leading-6',
|
||||
]"
|
||||
>
|
||||
Must be the same as above
|
||||
{{ $t("auth.register.confirmPasswordFormat") }}
|
||||
</p>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
<div>
|
||||
<LoadingButton type="submit" :loading="loading" class="w-full">
|
||||
Create
|
||||
{{ $t("create") }}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
|
||||
@@ -195,13 +195,15 @@
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import { type } from "arktype";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const invitationId = route.query.id?.toString();
|
||||
if (!invitationId)
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "Invitation required to sign up.",
|
||||
statusMessage: t("errors.inviteRequired"),
|
||||
});
|
||||
|
||||
const invitation = await $dropFetch(
|
||||
@@ -265,7 +267,7 @@ function register_wrapper() {
|
||||
router.push("/auth/signin");
|
||||
})
|
||||
.catch((response) => {
|
||||
const message = response.statusMessage || "An unknown error occurred";
|
||||
const message = response.statusMessage || t("errors.unknown");
|
||||
error.value = message;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -278,6 +280,6 @@ definePageMeta({
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Create your Drop account",
|
||||
title: t("auth.register.title"),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<h2
|
||||
class="mt-8 text-2xl font-bold font-display leading-9 tracking-tight text-zinc-100"
|
||||
>
|
||||
Sign in to your account
|
||||
{{ $t("auth.signin.title") }}
|
||||
</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-zinc-400">
|
||||
Don't have an account? Ask an admin to create one for you.
|
||||
{{ $t("auth.signin.noAccount") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
class="py-4 flex flex-row items-center justify-center gap-x-4 font-bold text-sm text-zinc-600"
|
||||
>
|
||||
<span class="h-[1px] grow bg-zinc-600" />
|
||||
OR
|
||||
{{ $t("auth.signin.or") }}
|
||||
<span class="h-[1px] grow bg-zinc-600" />
|
||||
</div>
|
||||
<AuthOpenID v-if="enabledAuths.includes('OpenID' as AuthMec)" />
|
||||
@@ -46,6 +46,7 @@
|
||||
import type { AuthMec } from "~/prisma/client";
|
||||
import DropLogo from "~/components/DropLogo.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const enabledAuths = await $dropFetch("/api/v1/auth");
|
||||
|
||||
definePageMeta({
|
||||
@@ -53,6 +54,6 @@ definePageMeta({
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Sign in to Drop",
|
||||
title: t("auth.signin.pageTitle"),
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
<CheckCircleIcon class="h-12 w-12 text-green-600" aria-hidden="true" />
|
||||
<div class="mt-3 text-center sm:mt-5">
|
||||
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
|
||||
Successful!
|
||||
{{ $t("auth.callback.success") }}
|
||||
</h1>
|
||||
<div class="mt-4">
|
||||
<p class="mx-auto text-sm text-zinc-400 max-w-sm">
|
||||
Drop has successfully authorized the client. You may now close this
|
||||
window.
|
||||
{{ $t("auth.callback.authorizedClient") }}
|
||||
</p>
|
||||
|
||||
<Disclosure v-slot="{ open }" as="div" class="mt-8">
|
||||
@@ -20,7 +19,9 @@
|
||||
<DisclosureButton
|
||||
class="pb-2 flex w-full items-start justify-between text-left text-zinc-400"
|
||||
>
|
||||
<span class="text-sm font-semibold">Having issues?</span>
|
||||
<span class="text-sm font-semibold">
|
||||
{{ $t("auth.callback.issues") }}
|
||||
</span>
|
||||
<span class="ml-6 flex h-7 items-center">
|
||||
<ChevronUpIcon
|
||||
v-if="!open"
|
||||
@@ -33,7 +34,7 @@
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" class="mt-2">
|
||||
<p class="text-zinc-100 font-semibold text-sm mb-3">
|
||||
Paste this code into the client to continue:
|
||||
{{ $t("auth.callback.paste") }}
|
||||
</p>
|
||||
<p
|
||||
class="max-w-sm text-nowrap overflow-x-auto text-sm bg-zinc-950/50 p-3 text-zinc-300 w-fit mx-auto rounded-xl"
|
||||
@@ -55,10 +56,10 @@
|
||||
<h1
|
||||
class="mt-4 text-3xl font-bold font-display tracking-tight text-zinc-100 sm:text-5xl"
|
||||
>
|
||||
Authorize client?
|
||||
{{ $t("auth.callback.authClient") }}
|
||||
</h1>
|
||||
<p class="mt-6 text-base leading-7 text-zinc-400">
|
||||
"{{ clientData.name }}" has requested access to your Drop account.
|
||||
{{ $t("auth.callback.requestedAccess", { name: clientData.name }) }}
|
||||
</p>
|
||||
<div
|
||||
action="/api/v1/client/callback"
|
||||
@@ -70,7 +71,7 @@
|
||||
class="rounded-md bg-blue-600 px-3.5 py-2.5 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"
|
||||
@click="() => authorize_wrapper()"
|
||||
>
|
||||
Authorize
|
||||
{{ $t("auth.callback.authorize") }}
|
||||
</button>
|
||||
|
||||
<div v-if="error" class="mt-5 rounded-md bg-red-600/10 p-4">
|
||||
@@ -93,9 +94,12 @@
|
||||
<p
|
||||
class="mt-6 font-semibold font-display text-lg leading-8 text-zinc-100"
|
||||
>
|
||||
Accepting this request will allow "{{ clientData.name }}" on "{{
|
||||
clientData.platform
|
||||
}}" to:
|
||||
{{
|
||||
$t("auth.callback.permWarning", {
|
||||
name: clientData.name,
|
||||
platform: clientData.platform,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-8 max-w-2xl sm:mt-12 lg:mt-14">
|
||||
@@ -123,8 +127,17 @@
|
||||
<NuxtLink
|
||||
:href="feature.href"
|
||||
class="text-sm font-semibold leading-6 text-blue-600"
|
||||
>Learn more <span aria-hidden="true">→</span></NuxtLink
|
||||
>
|
||||
<i18n-t
|
||||
keypath="auth.callback.learn"
|
||||
tag="span"
|
||||
scope="global"
|
||||
>
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="text-gray-100">Todo page</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
useHead({
|
||||
title: "Settings",
|
||||
});
|
||||
</script>
|
||||
+7
-4
@@ -42,7 +42,9 @@
|
||||
class="-m-2.5 p-2.5"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<span class="sr-only">{{
|
||||
$t("userHeader.closeSidebar")
|
||||
}}</span>
|
||||
<XMarkIcon class="size-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -73,13 +75,13 @@
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<Bars3Icon class="size-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div
|
||||
class="flex-1 text-sm/6 font-semibold uppercase font-display text-zinc-400"
|
||||
>
|
||||
Library
|
||||
{{ $t("userHeader.links.library") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +101,7 @@ import {
|
||||
} from "@headlessui/vue";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
@@ -107,7 +110,7 @@ router.afterEach(() => {
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "Library",
|
||||
title: t("userHeader.links.library"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
class="transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
||||
>
|
||||
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Back to Library
|
||||
{{ $t("library.back") }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">
|
||||
{{ collection?.name }}
|
||||
</h2>
|
||||
<p class="mt-2 text-zinc-400">
|
||||
{{ collection?.entries?.length || 0 }} games
|
||||
{{ $t("library.gameCount", collection?.entries?.length || 0) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -37,15 +37,19 @@ import { ArrowLeftIcon } from "@heroicons/vue/20/solid";
|
||||
|
||||
const route = useRoute();
|
||||
const collections = await useCollections();
|
||||
const { t } = useI18n();
|
||||
const collection = computed(() =>
|
||||
collections.value.find((e) => e.id == route.params.id),
|
||||
);
|
||||
if (collection.value === undefined) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Collection not found" });
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: t("library.collection.notFound"),
|
||||
});
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: collection.value?.name || "Collection",
|
||||
title: collection.value?.name || t("library.collection.title"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
class="transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
||||
>
|
||||
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Back to Library
|
||||
{{ $t("library.back") }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-blue-500/25 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
||||
>
|
||||
Open in Launcher
|
||||
{{ $t("library.launcherOpen") }}
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="-mr-0.5 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
@@ -57,7 +57,7 @@
|
||||
:to="`/store/${game.id}`"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3.5 py-2.5 text-base font-semibold font-display text-white shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
|
||||
>
|
||||
View in Store
|
||||
{{ $t("store.view") }}
|
||||
<ArrowUpRightIcon class="-mr-0.5 h-5 w-5" aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<div class="col-start-1 lg:col-start-2 space-y-6">
|
||||
<div class="bg-zinc-800/50 rounded-xl p-6 backdrop-blur-sm">
|
||||
<h2 class="text-xl font-display font-semibold text-zinc-100 mb-4">
|
||||
Game Images
|
||||
{{ $t("store.images") }}
|
||||
</h2>
|
||||
<div class="relative">
|
||||
<VueCarousel :items-to-show="1">
|
||||
@@ -86,7 +86,7 @@
|
||||
<div
|
||||
class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display"
|
||||
>
|
||||
No images
|
||||
{{ $t("store.noImages") }}
|
||||
</div>
|
||||
</VueSlide>
|
||||
|
||||
@@ -121,13 +121,15 @@ import {
|
||||
} from "@heroicons/vue/20/solid";
|
||||
import { micromark } from "micromark";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
const id = route.params.id.toString();
|
||||
|
||||
const { game: rawGame } = await $dropFetch(`/api/v1/games/${id}`);
|
||||
const game = computed(() => {
|
||||
if (!rawGame) {
|
||||
throw createError({ statusCode: 404, message: "Game not found" });
|
||||
throw createError({ statusCode: 404, message: t("library.notFound") });
|
||||
}
|
||||
return rawGame;
|
||||
});
|
||||
|
||||
+12
-8
@@ -2,10 +2,11 @@
|
||||
<div>
|
||||
<div class="flex flex-col gap-y-8">
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">Library</h2>
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">
|
||||
{{ $t("userHeader.links.library") }}
|
||||
</h2>
|
||||
<p class="mt-2 text-zinc-400">
|
||||
Organize your games into collections for easy access, and access all
|
||||
your games.
|
||||
{{ $t("library.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
{{ collection.name }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
{{ collection.entries.length }} game(s)
|
||||
{{ $t("library.gameCount", [collection.entries.length]) }}
|
||||
</p>
|
||||
</NuxtLink>
|
||||
|
||||
@@ -60,11 +61,11 @@
|
||||
<h3
|
||||
class="text-lg font-semibold text-zinc-400 group-hover:text-zinc-300"
|
||||
>
|
||||
Create Collection
|
||||
{{ $t("library.collection.create") }}
|
||||
</h3>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-zinc-500 group-hover:text-zinc-400">
|
||||
Add a new collection to organize your games
|
||||
{{ $t("library.collection.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
@@ -73,7 +74,9 @@
|
||||
|
||||
<!-- game library grid -->
|
||||
<div>
|
||||
<h1 class="text-zinc-100 text-xl font-bold font-display">All Games</h1>
|
||||
<h1 class="text-zinc-100 text-xl font-bold font-display">
|
||||
{{ $t("library.addGames") }}
|
||||
</h1>
|
||||
<div class="mt-4 flex flex-row flex-wrap justify-left gap-4">
|
||||
<GamePanel
|
||||
v-for="game in games"
|
||||
@@ -99,11 +102,12 @@ const collectionCreateOpen = ref(false);
|
||||
|
||||
const currentlyDeleting = ref<Collection | undefined>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const library = await useLibrary();
|
||||
const games = library.value.entries.map((e) => e.game);
|
||||
|
||||
useHead({
|
||||
title: "Home",
|
||||
title: t("userHeader.links.library"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+7
-4
@@ -42,7 +42,9 @@
|
||||
class="-m-2.5 p-2.5"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<span class="sr-only">Close sidebar</span>
|
||||
<span class="sr-only">
|
||||
{{ $t("userHeader.closeSidebar") }}
|
||||
</span>
|
||||
<XMarkIcon class="size-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -73,13 +75,13 @@
|
||||
class="-m-2.5 p-2.5 text-zinc-400 lg:hidden"
|
||||
@click="sidebarOpen = true"
|
||||
>
|
||||
<span class="sr-only">Open sidebar</span>
|
||||
<span class="sr-only">{{ $t("header.openSidebar") }}</span>
|
||||
<Bars3Icon class="size-6" aria-hidden="true" />
|
||||
</button>
|
||||
<div
|
||||
class="flex-1 text-sm/6 font-semibold uppercase font-display text-zinc-400"
|
||||
>
|
||||
News
|
||||
{{ $t("userHeader.links.news") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,6 +108,7 @@ if (!news.value) {
|
||||
console.log("fetched news");
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const sidebarOpen = ref(false);
|
||||
|
||||
@@ -114,7 +117,7 @@ router.afterEach(() => {
|
||||
});
|
||||
|
||||
useHead({
|
||||
title: "News",
|
||||
title: t("userHeader.links.news"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
class="px-2 py-1 rounded bg-zinc-900/80 backdrop-blur-sm transition text-sm/6 font-semibold text-zinc-400 hover:text-zinc-100 inline-flex gap-x-2 items-center duration-200 hover:scale-105"
|
||||
>
|
||||
<ArrowLeftIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Back to News
|
||||
{{ $t("news.back") }}
|
||||
</NuxtLink>
|
||||
|
||||
<button
|
||||
@@ -37,7 +37,7 @@
|
||||
@click="() => (currentlyDeleting = article)"
|
||||
>
|
||||
<TrashIcon class="h-4 w-4" aria-hidden="true" />
|
||||
Delete Article
|
||||
{{ $t("news.delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +91,7 @@ const route = useRoute();
|
||||
const currentlyDeleting = ref();
|
||||
const user = useUser();
|
||||
const news = useNews();
|
||||
const { t } = useI18n();
|
||||
if (!news.value) {
|
||||
news.value = await fetchNews();
|
||||
}
|
||||
@@ -100,7 +101,7 @@ const article = computed(() =>
|
||||
if (!article.value)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Article not found",
|
||||
statusMessage: t("news.notFound"),
|
||||
fatal: true,
|
||||
});
|
||||
|
||||
|
||||
+10
-14
@@ -4,10 +4,10 @@
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold font-display text-zinc-100">
|
||||
Latest News
|
||||
{{ $t("news.title") }}
|
||||
</h2>
|
||||
<p class="mt-2 text-zinc-400">
|
||||
Stay up to date with the latest updates and announcements.
|
||||
{{ $t("news.subheader") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
:datetime="article.publishedAt"
|
||||
class="text-sm text-zinc-400"
|
||||
>
|
||||
{{ formatDate(article.publishedAt) }}
|
||||
{{ $d(new Date(article.publishedAt), "short") }}
|
||||
</time>
|
||||
<span class="text-sm text-blue-400">{{
|
||||
article.author?.displayName ?? "System"
|
||||
@@ -73,8 +73,10 @@
|
||||
|
||||
<div v-if="articles?.length === 0" class="text-center py-12">
|
||||
<DocumentIcon class="mx-auto h-12 w-12 text-zinc-400" />
|
||||
<h3 class="mt-2 text-sm font-semibold text-zinc-100">No articles</h3>
|
||||
<p class="mt-1 text-sm text-zinc-500">Check back later for updates.</p>
|
||||
<h3 class="mt-2 text-sm font-semibold text-zinc-100">
|
||||
{{ $t("news.none") }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-zinc-500">{{ $t("news.checkLater") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -84,6 +86,8 @@ import { DocumentIcon } from "@heroicons/vue/24/outline";
|
||||
import type { Article } from "~/prisma/client";
|
||||
import type { SerializeObject } from "nitropack/types";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { articles } = defineProps<{
|
||||
articles: SerializeObject<
|
||||
Article & {
|
||||
@@ -93,16 +97,8 @@ const { articles } = defineProps<{
|
||||
>[];
|
||||
}>();
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString("en-AU", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
useHead({
|
||||
title: "News",
|
||||
title: t("userHeader.links.news"),
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+15
-14
@@ -44,7 +44,7 @@
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-2 rounded-md bg-zinc-800 px-3 py-1 text-sm font-semibold font-display text-white shadow-sm hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 duration-200 hover:scale-105 active:scale-95"
|
||||
>
|
||||
Open in Admin Dashboard
|
||||
{{ $t("store.openAdminDashboard") }}
|
||||
<ArrowTopRightOnSquareIcon
|
||||
class="-mr-0.5 h-7 w-7 p-1"
|
||||
aria-hidden="true"
|
||||
@@ -56,19 +56,19 @@
|
||||
<td
|
||||
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
|
||||
>
|
||||
Released
|
||||
{{ $t("store.released") }}
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">
|
||||
{{
|
||||
DateTime.fromISO(game.mReleased).toFormat("d MMMM, yyyy")
|
||||
}}
|
||||
<time datetime="game.mReleased">
|
||||
{{ $d(new Date(game.mReleased), "short") }}
|
||||
</time>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
|
||||
>
|
||||
Platform(s)
|
||||
{{ $t("store.platform", platforms.length) }}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap inline-flex gap-x-4 px-3 py-4 text-sm text-zinc-400"
|
||||
@@ -82,7 +82,7 @@
|
||||
<span
|
||||
v-if="platforms.length == 0"
|
||||
class="font-semibold text-blue-600"
|
||||
>coming soon</span
|
||||
>{{ $t("store.commingSoon") }}</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -90,7 +90,7 @@
|
||||
<td
|
||||
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-zinc-100 sm:pl-3"
|
||||
>
|
||||
Rating
|
||||
{{ $t("store.rating") }}
|
||||
</td>
|
||||
<td
|
||||
class="whitespace-nowrap flex flex-row items-center gap-x-1 px-3 py-4 text-sm text-zinc-400"
|
||||
@@ -103,9 +103,9 @@
|
||||
'w-4 h-4',
|
||||
]"
|
||||
/>
|
||||
<span class="text-zinc-600"
|
||||
>({{ rating._sum.mReviewCount ?? 0 }} reviews)</span
|
||||
>
|
||||
<span class="text-zinc-600">{{
|
||||
$t("store.reviews", [rating._sum.mReviewCount ?? 0])
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -131,7 +131,7 @@
|
||||
<div
|
||||
class="h-48 lg:h-96 aspect-[1/2] flex items-center justify-center text-zinc-700 font-bold font-display"
|
||||
>
|
||||
No images
|
||||
{{ $t("store.noImages") }}
|
||||
</div>
|
||||
</VueSlide>
|
||||
|
||||
@@ -162,7 +162,9 @@
|
||||
<div class="grow h-[1px] bg-zinc-700 rounded-full" />
|
||||
<span
|
||||
class="uppercase text-sm font-semibold font-display text-zinc-600"
|
||||
>Click to read {{ showPreview ? "more" : "less" }}</span
|
||||
>{{
|
||||
showPreview ? $t("store.readMore") : $t("store.readLess")
|
||||
}}</span
|
||||
>
|
||||
<div class="grow h-[1px] bg-zinc-700 rounded-full" />
|
||||
</button>
|
||||
@@ -177,7 +179,6 @@
|
||||
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/24/outline";
|
||||
import { StarIcon } from "@heroicons/vue/24/solid";
|
||||
import { micromark } from "micromark";
|
||||
import { DateTime } from "luxon";
|
||||
import type { PlatformClient } from "~/composables/types";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
+26
-12
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="w-full flex flex-col overflow-x-hidden">
|
||||
<DevOnly
|
||||
><h1 class="text-gray-100">{{ $t("welcome") }}</h1>
|
||||
</DevOnly>
|
||||
|
||||
<!-- Hero section -->
|
||||
<VueCarousel
|
||||
v-if="recent.length > 0"
|
||||
@@ -24,7 +28,7 @@
|
||||
>
|
||||
<div class="relative text-center">
|
||||
<h3 class="text-base/7 font-semibold text-blue-300">
|
||||
Recently added
|
||||
{{ $t("store.recentlyAdded") }}
|
||||
</h3>
|
||||
<h2
|
||||
class="text-3xl font-bold tracking-tight text-white sm:text-5xl"
|
||||
@@ -41,7 +45,7 @@
|
||||
<NuxtLink
|
||||
:href="`/store/${game.id}`"
|
||||
class="block w-full rounded-md border border-transparent bg-white px-8 py-3 text-base font-medium text-gray-900 hover:bg-gray-100 sm:w-auto duration-200 hover:scale-105"
|
||||
>Check it out</NuxtLink
|
||||
>{{ $t("store.lookAt") }}</NuxtLink
|
||||
>
|
||||
<AddLibraryButton :game-id="game.id" />
|
||||
</div>
|
||||
@@ -62,18 +66,22 @@
|
||||
<h2
|
||||
class="uppercase text-xl font-bold tracking-tight text-zinc-700 sm:text-3xl"
|
||||
>
|
||||
no game
|
||||
{{ $t("store.noGame") }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- new releases -->
|
||||
<div class="px-4 sm:px-12 py-4">
|
||||
<h1 class="text-zinc-100 text-2xl font-bold font-display">
|
||||
Recently released
|
||||
{{ $t("store.recentlyReleased") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold"
|
||||
>Explore more →</NuxtLink
|
||||
>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<div class="mt-4">
|
||||
<GameCarousel :items="released" :min="12" />
|
||||
</div>
|
||||
@@ -82,11 +90,15 @@
|
||||
<!-- recently updated -->
|
||||
<div class="px-4 sm:px-12 py-4" hydrate-on-visible>
|
||||
<h1 class="text-zinc-100 text-2xl font-bold font-display">
|
||||
Recently updated
|
||||
{{ $t("store.recentlyUpdated") }}
|
||||
</h1>
|
||||
<NuxtLink class="text-blue-600 font-semibold"
|
||||
>Explore more →</NuxtLink
|
||||
>
|
||||
<NuxtLink class="text-blue-600 font-semibold">
|
||||
<i18n-t keypath="store.exploreMore" tag="span">
|
||||
<template #arrow>
|
||||
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</NuxtLink>
|
||||
<div class="mt-4">
|
||||
<GameCarousel :items="updated" :min="12" />
|
||||
</div>
|
||||
@@ -102,7 +114,9 @@ const released = await $dropFetch("/api/v1/store/released");
|
||||
// const developers = await $dropFetch("/api/v1/store/developers");
|
||||
// const publishers = await $dropFetch("/api/v1/store/publishers");
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
useHead({
|
||||
title: "Store",
|
||||
title: t("store.title"),
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user