Make application and logo configurable (#336)

* Adds settings for server name and logo

* Implements ApplicationLogo and replaces site name based on settings

* Refactors component for changing the company logo

* Removes unused variable

* Uses message instead of statusMessage

* Replaces favicon with logo if set
This commit is contained in:
Paco
2026-02-06 00:43:21 +00:00
committed by GitHub
parent 15f5986b07
commit af08472e45
27 changed files with 452 additions and 102 deletions
+1 -2
View File
@@ -18,7 +18,7 @@
<TileWithLink>
<div class="h-full flex">
<div class="flex-1 my-auto">
<DropLogo />
<ApplicationLogo />
</div>
<div
class="flex-6 lg:flex-2 my-auto text-center flex lg:inline mx-4"
@@ -192,7 +192,6 @@
<script setup lang="ts">
import { formatBytes } from "~/server/internal/utils/files";
import GamepadIcon from "~/components/Icons/GamepadIcon.vue";
import DropLogo from "~/components/DropLogo.vue";
import { ServerStackIcon, UserGroupIcon } from "@heroicons/vue/24/outline";
import { getPercentage } from "~/utils/utils";
import { getBarColor } from "~/utils/colors";
@@ -11,18 +11,12 @@
<div class="relative inline-flex items-center gap-4">
<!-- icon image -->
<div class="relative group/iconupload rounded-xl overflow-hidden">
<img :src="useObject(company.mLogoObjectId)" class="size-20" />
<button
class="rounded-xl transition duration-200 absolute inset-0 opacity-0 group-hover/iconupload:opacity-100 focus-visible/iconupload:opacity-100 cursor-pointer bg-zinc-900/80 text-zinc-100 flex flex-col items-center justify-center text-center text-xs font-semibold ring-1 ring-inset ring-zinc-800 px-2"
@click="() => (uploadLogoOpen = true)"
>
<ArrowUpTrayIcon class="size-5" />
<span>{{
$t("library.admin.metadata.companies.editor.uploadIcon")
}}</span>
</button>
</div>
<ImageUpload
:object-id="company.mLogoObjectId"
:open-modal="() => (uploadLogoOpen = true)"
:hover-text="$t('library.admin.metadata.companies.editor.uploadIcon')"
:image-alt="`${company.mName} logo`"
/>
<div class="flex flex-col">
<h1
class="group/name inline-flex items-center gap-x-3 text-5xl font-bold font-display text-zinc-100"
+9 -2
View File
@@ -43,13 +43,20 @@ import {
BuildingStorefrontIcon,
CodeBracketIcon,
ServerIcon,
ServerStackIcon,
} from "@heroicons/vue/24/outline";
const navigation: Array<NavigationItem & { icon: Component }> = [
{
label: $t("header.admin.settings.store"),
label: $t("header.admin.settings.general"),
route: "/admin/settings",
prefix: "/admin/settings",
icon: ServerIcon,
},
{
label: $t("header.admin.settings.store"),
route: "/admin/settings/store",
prefix: "/admin/settings/store",
icon: BuildingStorefrontIcon,
},
{
@@ -62,7 +69,7 @@ const navigation: Array<NavigationItem & { icon: Component }> = [
label: "Services",
route: "/admin/settings/services",
prefix: "/admin/settings/services",
icon: ServerIcon,
icon: ServerStackIcon,
},
];
+124 -58
View File
@@ -1,59 +1,103 @@
<template>
<form class="space-y-4" @submit.prevent="() => saveSettings()">
<div class="pb-4 border-b border-zinc-700">
<h2 class="text-xl font-semibold text-zinc-100">
{{ $t("settings.admin.store.title") }}
</h2>
<div>
<form class="space-y-4" @submit.prevent="() => saveSettings()">
<div class="pb-4 border-b border-zinc-700 w-2xl mt-2">
<h2 class="text-xl font-semibold text-zinc-100">
{{ $t("settings.admin.general.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="mt-4">
<label
for="serverName"
class="block text-sm/6 font-medium text-zinc-100"
>{{ $t("settings.admin.general.serverName") }}</label
>
<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>
<div class="mt-2">
<input
id="name"
v-model="settings.generalSettings.serverName"
type="text"
name="serverName"
:placeholder="$t('settings.admin.general.serverNamePlaceholder')"
class="block w-full rounded-md bg-zinc-800 px-3 py-1.5 text-base text-zinc-100 outline outline-1 -outline-offset-1 outline-zinc-700 placeholder:text-zinc-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-blue-600 sm:text-sm/6"
@input="(event) => updateServerName(event)"
/>
</div>
</div>
<LoadingButton
type="submit"
class="inline-flex w-full shadow-sm sm:w-auto"
:loading="saving"
:disabled="!allowSave"
>
{{ allowSave ? $t("common.save") : $t("common.saved") }}
</LoadingButton>
</form>
<div class="mt-4">
<p for="logo" class="block text-sm/6 font-medium text-zinc-100">
{{ $t("settings.admin.general.logo") }}
</p>
<ul class="flex gap-3">
<li class="w-40 flex flex-col items-center">
<div class="flex items-center max-w-25 mt-2 mb-2 h-full">
<ImageUpload
:hover-text="$t('settings.admin.general.uploadLogo')"
:open-modal="openModal"
:object-id="mCustomLogoObjectId"
:image-alt="$t('settings.admin.general.applicationLogo')"
/>
</div>
<label class="flex flex-col text-zinc-100 text-sm items-center">
<div class="flex items-center">
<input
v-model="settings.generalSettings.mLogoObjectId"
class="mr-1"
type="radio"
name="mLogoObjectId"
:value="mCustomLogoObjectId"
@input="updateFormLogo"
/>
{{ $t("settings.admin.general.customLogo") }}
</div>
</label>
</li>
<li class="w-40 flex flex-col items-center">
<div class="flex w-25 mt-2 mb-2 h-full">
<DropLogo @click="() => updateFormLogo(null)" />
</div>
<label class="flex flex-col text-zinc-100 text-sm items-center">
<div class="flex items-center">
<input
v-model="settings.generalSettings.mLogoObjectId"
class="mr-1"
type="radio"
name="isDefaultLogo"
:checked="settings.generalSettings.mLogoObjectId === null"
:value="null"
@input="() => updateFormLogo(null)"
/>
{{ $t("settings.admin.general.defaultLogo") }}
</div>
</label>
</li>
</ul>
</div>
</div>
<ModalUploadFile
v-model="uploadLogoOpen"
:endpoint="`/api/v1/admin/settings/logo`"
accept="image/*"
@upload="updateLogo"
/>
<LoadingButton
type="submit"
class="inline-flex w-full shadow-sm sm:w-auto"
:loading="saving"
:disabled="!allowSave"
>
{{ allowSave ? $t("common.save") : $t("common.saved") }}
</LoadingButton>
</form>
</div>
</template>
<script setup lang="ts">
import { FetchError } from "ofetch";
import type { Settings } from "~/server/internal/utils/types";
const { t } = useI18n();
@@ -65,30 +109,40 @@ useHead({
title: t("settings.admin.title"),
});
const settings = await $dropFetch("/api/v1/settings");
const { game } = await $dropFetch("/api/v1/admin/settings/dummy-data");
const settings = ref<Settings>(await $dropFetch("/api/v1/settings"));
const allowSave = ref(false);
const allowSave = ref<boolean>(false);
const uploadLogoOpen = ref<boolean>(false);
const showGamePanelTextDecoration = ref<boolean>(
settings.showGamePanelTextDecoration,
const mCustomLogoObjectId = ref<string>(
settings.value.generalSettings.mLogoObjectId || "",
);
function setShowTitleDescription(value: boolean) {
showGamePanelTextDecoration.value = value;
const updateServerName = (event: InputEvent) => {
settings.value.generalSettings.serverName =
(event.target as HTMLInputElement)?.value || "";
allowSave.value = true;
}
};
const openModal = () => {
uploadLogoOpen.value = true;
};
const saving = ref<boolean>(false);
async function saveSettings() {
saving.value = true;
try {
await $dropFetch("/api/v1/admin/settings", {
settings.value = await $dropFetch("/api/v1/admin/settings", {
method: "PATCH",
body: {
showGamePanelTextDecoration: showGamePanelTextDecoration.value,
generalSettings: {
serverName: settings.value.generalSettings.serverName,
mLogoObjectId: settings.value.generalSettings.mLogoObjectId,
},
},
});
window.location.reload();
} catch (e) {
createModal(
ModalType.Notification,
@@ -105,4 +159,16 @@ async function saveSettings() {
saving.value = false;
allowSave.value = false;
}
function updateLogo(response: { id: string }) {
mCustomLogoObjectId.value = response.id;
settings.value.generalSettings.mLogoObjectId = response.id;
allowSave.value = true;
}
const updateFormLogo = (event: InputEvent | null) => {
settings.value.generalSettings.mLogoObjectId =
(event?.target as HTMLInputElement)?.value || null;
allowSave.value = true;
};
</script>
+110
View File
@@ -0,0 +1,110 @@
<template>
<form class="space-y-4" @submit.prevent="() => saveSettings()">
<div class="pb-4 border-b 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("common.save") : $t("common.saved") }}
</LoadingButton>
</form>
</template>
<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/settings");
const { game } = await $dropFetch("/api/v1/admin/settings/dummy-data");
const allowSave = ref(false);
const showGamePanelTextDecoration = ref<boolean>(
settings.store.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: {
store: {
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>