Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion components/GameCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
:key="gameIdx"
class="justify-start"
>
<GamePanel :game="game" />
<GamePanel
:game="game"
:href="game ? `/store/${game.id}` : undefined"
:show-title-description="showGamePanelTextDecoration"
/>
</VueSlide>

<template #addons>
Expand Down Expand Up @@ -40,6 +44,10 @@ const props = defineProps<{
width?: number;
}>();

const { showGamePanelTextDecoration } = await $dropFetch(
`/api/v1/admin/settings`,
);

const currentComponent = ref<HTMLDivElement>();

const min = computed(() => Math.max(props.min ?? 8, props.items.length));
Expand Down
74 changes: 57 additions & 17 deletions components/GamePanel.vue
Original file line number Diff line number Diff line change
@@ -1,56 +1,96 @@
<template>
<NuxtLink
v-if="game"
:href="props.href ?? `/store/${game.id}`"
class="group relative w-48 h-64 rounded-lg overflow-hidden transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5"
@click="active = game.id"
v-if="game || defaultPlaceholder"
:href="href"
:class="{
'transition-all duration-300 text-left hover:scale-[1.02] hover:shadow-lg hover:-translate-y-0.5':
animate,
}"
class="group relative w-48 h-64 rounded-lg overflow-hidden"
>
<div
class="absolute inset-0 transition-all duration-300 group-hover:scale-110"
:class="{
'transition-all duration-300 group-hover:scale-110': animate,
}"
class="absolute inset-0"
>
<img
:src="useObject(game.mCoverObjectId)"
:src="imageProps.src"
class="w-full h-full object-cover brightness-[90%]"
:class="{ active: active === game.id }"
:alt="game.mName"
:alt="imageProps.alt"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-zinc-950/80 via-zinc-950/20 to-transparent"
/>
</div>

<div class="absolute bottom-0 left-0 w-full p-3">
<div
v-if="showTitleDescription"
class="absolute bottom-0 left-0 w-full p-3"
>
<h1
class="text-zinc-100 text-sm font-bold font-display group-hover:text-white transition-colors"
:class="{ 'group-hover:text-white transition-colors': animate }"
class="text-zinc-100 text-sm font-bold font-display"
>
{{ game.mName }}
{{ game ? game.mName : $t("settings.admin.store.dropGameNamePlaceholder") }}
</h1>
<p
class="text-zinc-400 text-xs line-clamp-2 group-hover:text-zinc-300 transition-colors"
:class="{
'group-hover:text-zinc-300 transition-colors': animate,
}"
class="text-zinc-400 text-xs line-clamp-2"
>
{{ game.mShortDescription }}
{{
game
? game.mShortDescription
: $t("settings.admin.store.dropGameDescriptionPlaceholder")
}}
</p>
</div>
</NuxtLink>
<SkeletonCard v-else :message="$t('store.noGame')" />
<SkeletonCard
v-else-if="defaultPlaceholder === false"
:message="$t('store.noGame')"
/>
</template>

<script setup lang="ts">
import type { SerializeObject } from "nitropack";

const props = defineProps<{
const { t } = useI18n();
const {
game,
href = undefined,
showTitleDescription = true,
animate = true,
defaultPlaceholder = false,
} = defineProps<{
game:
| SerializeObject<{
id: string;
mCoverObjectId: string;
mName: string;
mShortDescription: string;
}>
| undefined;
| undefined
| null;
href?: string;
showTitleDescription?: boolean;
animate?: boolean;
defaultPlaceholder?: boolean;
}>();

const active = useState();
const imageProps = {
src: "",
alt: t("settings.store.dropGameAltPlaceholder"),

Check warning on line 85 in components/GamePanel.vue

View workflow job for this annotation

GitHub Actions / Lint

'settings.store' does not exist in localization message resources
};

if (game) {
imageProps.src = useObject(game.mCoverObjectId);
imageProps.alt = game.mName;
} else if (defaultPlaceholder) {
imageProps.src = "/game-panel-placeholder.png";
}
</script>

<style scoped>
Expand Down
22 changes: 22 additions & 0 deletions components/OptionWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div
:class="[
'transition border border-3 rounded-xl relative cursor-pointer',
active ? 'border-blue-600' : 'border-zinc-700',
]"
>
<div v-if="active" class="absolute top-1 right-1 z-1">
<CheckIcon
class="rounded-full p-1.5 bg-blue-600 size-6 text-transparent stroke-3 stroke-zinc-900 font-bold"
/>
</div>

<slot />
</div>
</template>

<script setup lang="ts">
import { CheckIcon } from '@heroicons/vue/24/solid';

const { active = false } = defineProps<{ active?: boolean }>();
</script>
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default withNuxt([
// Optional.
"@intlify/vue-i18n/no-dynamic-keys": "error",
"@intlify/vue-i18n/no-unused-keys": [
"error",
"off",
{
extensions: [".js", ".vue", ".ts"],
},
Expand Down
19 changes: 17 additions & 2 deletions i18n/locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
"admin": {
"admin": "Admin",
"tasks": "Tasks",
"users": "Users"
"users": "Users",
"settings": "Settings"
},
"back": "Back",
"openSidebar": "Open sidebar"
Expand Down Expand Up @@ -462,10 +463,24 @@
},
"options": "Options",
"save": "Save",
"saved": "Saved",
"add": "Add",
"insert": "Insert",
"security": "Security",
"settings": "Settings",
"settings": {
"admin": {
"title": "Settings",
"description": "Configure Drop settings",

"store": {
"title": "Store",
"showGamePanelTextDecoration": "Show title and description on game tiles (default: on)",
"dropGameNamePlaceholder": "Example Game",
"dropGameDescriptionPlaceholder": "This is an example game. It will be replaced if you import a game.",
"dropGameAltPlaceholder": "Example Game icon"
}
}
},
"store": {
"commingSoon": "coming soon",
"exploreMore": "Explore more {arrow}",
Expand Down
2 changes: 1 addition & 1 deletion layouts/admin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
icon: RectangleStackIcon,
},
{
label: $t("settings"),
label: $t("header.admin.settings"),
route: "/admin/settings",
prefix: "/admin/settings",
icon: Cog6ToothIcon,
Expand Down
119 changes: 114 additions & 5 deletions pages/admin/settings/index.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,121 @@
<template>
<div class="text-gray-100">{{ $t("todo") }}</div>
<div class="space-y-4">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-zinc-100">
{{ $t("settings.admin.title") }}
</h1>
<p class="mt-2 text-base text-zinc-400">
{{ $t("settings.admin.description") }}
</p>
</div>
</div>

<form class="space-y-4" @submit.prevent="() => saveSettings()">
<div class="py-6 border-y border-zinc-700">
<h2 class="text-xl font-semibold text-zinc-100">
{{ $t("settings.admin.store.title") }}
</h2>

<h3 class="text-base font-medium text-zinc-400 mb-3 m-x-0">
{{ $t("settings.admin.store.showGamePanelTextDecoration") }}
</h3>
<ul class="flex gap-3">
<li class="inline-block">
<OptionWrapper
:active="showGamePanelTextDecoration"
@click="setShowTitleDescription(true)"
>
<div class="flex">
<GamePanel
:animate="false"
:game="game"
:default-placeholder="true"
/>
</div>
</OptionWrapper>
</li>
<li class="inline-block">
<OptionWrapper
:active="!showGamePanelTextDecoration"
@click="setShowTitleDescription(false)"
>
<div class="flex">
<GamePanel
:game="game"
:show-title-description="false"
:animate="false"
:default-placeholder="true"
/>
</div>
</OptionWrapper>
</li>
</ul>
</div>

<LoadingButton
type="submit"
class="inline-flex w-full shadow-sm sm:w-auto"
:loading="saving"
:disabled="!allowSave"
>
{{ allowSave ? $t("save") : $t("saved") }}
</LoadingButton>
</form>
</div>
</template>
<script lang="ts" setup>
useHead({
title: "Settings",
});

<script setup lang="ts">
import { FetchError } from "ofetch";

const { t } = useI18n();

definePageMeta({
layout: "admin",
});

useHead({
title: t("settings.admin.title"),
});

const settings = await $dropFetch("/api/v1/admin/settings");
const { game } = await $dropFetch("/api/v1/admin/settings/dummy-data");

const allowSave = ref(false);

const showGamePanelTextDecoration = ref<boolean>(
settings.showGamePanelTextDecoration,
);

function setShowTitleDescription(value: boolean) {
showGamePanelTextDecoration.value = value;
allowSave.value = true;
}

const saving = ref<boolean>(false);
async function saveSettings() {
saving.value = true;
try {
await $dropFetch("/api/v1/admin/settings", {
method: "PATCH",
body: {
showGamePanelTextDecoration: showGamePanelTextDecoration.value,
},
});
} catch (e) {
createModal(
ModalType.Notification,
{
title: `Failed to save settings.`,
description:
e instanceof FetchError
? (e.statusMessage ?? e.message)
: (e as string).toString(),
},
(_, c) => c(),
);
}
saving.value = false;
allowSave.value = false;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApplicationSettings" ADD COLUMN "showGamePanelTextDecoration" BOOLEAN NOT NULL DEFAULT true;
2 changes: 2 additions & 0 deletions prisma/models/app.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ model ApplicationSettings {
saveSlotCountLimit Int @default(5)
saveSlotSizeLimit Float @default(10) // MB
saveSlotHistoryLimit Int @default(3)

showGamePanelTextDecoration Boolean @default(true)
}

enum Platform {
Expand Down
26 changes: 13 additions & 13 deletions prisma/models/client.prisma
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
enum ClientCapabilities {
PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client
UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc)
CloudSaves @map("cloudSaves") // ability to save to save slots
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
PeerAPI @map("peerAPI") // other clients can use the HTTP API to P2P with this client
UserStatus @map("userStatus") // this client can report this user's status (playing, online, etc etc)
CloudSaves @map("cloudSaves") // ability to save to save slots
TrackPlaytime @map("trackPlaytime") // ability to track user playtime
}

// References a device
model Client {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

capabilities ClientCapabilities[]
capabilities ClientCapabilities[]

name String
platform Platform
lastConnected DateTime
name String
platform Platform
lastConnected DateTime

lastAccessedSaves SaveSlot[]
tokens APIToken[]
lastAccessedSaves SaveSlot[]
tokens APIToken[]
}
Loading
Loading