This patch makes the border around the "dark" and "light" radio button to always be present. Previously it was not present when the button were disabled because using the system theme, which caused it to make the elements larger and made them move when toggling "adapt to system theme" To keep the same meaning as previously, the border is only colored when the button is activated Fix #1339
345 lines
9.7 KiB
Vue
345 lines
9.7 KiB
Vue
<template>
|
|
<div>
|
|
<breadcrumbs-nav
|
|
:links="[
|
|
{
|
|
name: RouteName.ACCOUNT_SETTINGS,
|
|
text: t('Account'),
|
|
},
|
|
{
|
|
name: RouteName.PREFERENCES,
|
|
text: t('Preferences'),
|
|
},
|
|
]"
|
|
/>
|
|
<div>
|
|
<o-field :label="t('Theme')" addonsClass="flex flex-col">
|
|
<o-field>
|
|
<o-checkbox v-model="systemTheme">{{
|
|
t("Adapt to system theme")
|
|
}}</o-checkbox>
|
|
</o-field>
|
|
<o-field>
|
|
<fieldset>
|
|
<legend class="sr-only">{{ t("Theme") }}</legend>
|
|
<o-radio
|
|
:class="{ 'border-mbz-bluegreen': theme === 'light' }"
|
|
class="p-4 bg-white text-zinc-800 rounded-md mt-2 mr-2 border-2"
|
|
:disabled="systemTheme"
|
|
v-model="theme"
|
|
name="theme"
|
|
native-value="light"
|
|
>{{ t("Light") }}</o-radio
|
|
>
|
|
<o-radio
|
|
:class="{ 'border-mbz-bluegreen': theme === 'dark' }"
|
|
class="p-4 bg-zinc-800 rounded-md text-white mt-2 ml-2 border-2"
|
|
:disabled="systemTheme"
|
|
v-model="theme"
|
|
name="theme"
|
|
native-value="dark"
|
|
>{{ t("Dark") }}</o-radio
|
|
>
|
|
</fieldset>
|
|
</o-field>
|
|
</o-field>
|
|
<o-field :label="t('Language')" label-for="setting-language">
|
|
<o-select
|
|
:loading="loadingTimezones || loadingUserSettings"
|
|
v-model="$i18n.locale"
|
|
:placeholder="t('Select a language')"
|
|
id="setting-language"
|
|
>
|
|
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
|
|
{{ language }}
|
|
</option>
|
|
</o-select>
|
|
</o-field>
|
|
<o-field
|
|
:label="t('Timezone')"
|
|
v-if="selectedTimezone"
|
|
label-for="setting-timezone"
|
|
>
|
|
<o-select
|
|
:placeholder="t('Select a timezone')"
|
|
:loading="loadingTimezones || loadingUserSettings"
|
|
v-model="selectedTimezone"
|
|
id="setting-timezone"
|
|
>
|
|
<optgroup
|
|
:label="group"
|
|
v-for="(groupTimezones, group) in timezones"
|
|
:key="group"
|
|
>
|
|
<option
|
|
v-for="timezone in groupTimezones"
|
|
:value="`${group}/${timezone}`"
|
|
:key="timezone"
|
|
>
|
|
{{ sanitize(timezone) }}
|
|
</option>
|
|
</optgroup>
|
|
</o-select>
|
|
</o-field>
|
|
<em v-if="Intl.DateTimeFormat().resolvedOptions().timeZone">{{
|
|
t("Timezone detected as {timezone}.", {
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
})
|
|
}}</em>
|
|
<o-notification v-else variant="danger">{{
|
|
t("Unable to detect timezone.")
|
|
}}</o-notification>
|
|
<hr role="presentation" />
|
|
<o-field grouped>
|
|
<o-field :label="t('City or region')" expanded label-for="setting-city">
|
|
<full-address-auto-complete
|
|
v-if="loggedUser?.settings"
|
|
:resultType="AddressSearchType.ADMINISTRATIVE"
|
|
v-model="address"
|
|
:default-text="address?.description"
|
|
id="setting-city"
|
|
class="grid"
|
|
:hideMap="true"
|
|
:hideSelected="true"
|
|
labelClass="sr-only"
|
|
:placeholder="t('e.g. Nantes, Berlin, Cork, …')"
|
|
/>
|
|
</o-field>
|
|
<o-field :label="t('Radius')" label-for="setting-radius">
|
|
<o-select
|
|
:placeholder="t('Select a radius')"
|
|
v-model="locationRange"
|
|
id="setting-radius"
|
|
>
|
|
<option
|
|
v-for="index in [1, 5, 10, 25, 50, 100]"
|
|
:key="index"
|
|
:value="index"
|
|
>
|
|
{{ t("{count} km", { count: index }, index) }}
|
|
</option>
|
|
</o-select>
|
|
</o-field>
|
|
<o-button
|
|
:disabled="address == undefined"
|
|
@click="resetArea"
|
|
@keyup.enter="resetArea"
|
|
class="reset-area self-center"
|
|
icon-left="close"
|
|
:aria-label="t('Reset')"
|
|
/>
|
|
</o-field>
|
|
<p>
|
|
{{
|
|
t(
|
|
"Your city or region and the radius will only be used to suggest you events nearby. The event radius will consider the administrative center of the area."
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import ngeohash from "ngeohash";
|
|
import { USER_SETTINGS, SET_USER_SETTINGS } from "../../graphql/user";
|
|
import langs from "../../i18n/langs.json";
|
|
import RouteName from "../../router/name";
|
|
import { AddressSearchType } from "@/types/enums";
|
|
import { Address, IAddress } from "@/types/address.model";
|
|
import { useTimezones } from "@/composition/apollo/config";
|
|
import { useUserSettings } from "@/composition/apollo/user";
|
|
import { useHead } from "@vueuse/head";
|
|
import { computed, defineAsyncComponent, ref, watch } from "vue";
|
|
import { useI18n } from "vue-i18n";
|
|
import { useMutation } from "@vue/apollo-composable";
|
|
|
|
const FullAddressAutoComplete = defineAsyncComponent(
|
|
() => import("@/components/Event/FullAddressAutoComplete.vue")
|
|
);
|
|
|
|
const { timezones: serverTimezones, loading: loadingTimezones } =
|
|
useTimezones();
|
|
const { loggedUser, loading: loadingUserSettings } = useUserSettings();
|
|
|
|
const { t } = useI18n({ useScope: "global" });
|
|
|
|
useHead({
|
|
title: computed(() => t("Preferences")),
|
|
});
|
|
|
|
// langs: Record<string, string> = langs;
|
|
|
|
const theme = ref(localStorage.getItem("theme"));
|
|
const systemTheme = ref(!("theme" in localStorage));
|
|
|
|
watch(systemTheme, (newSystemTheme) => {
|
|
console.debug("changing system theme", newSystemTheme);
|
|
if (newSystemTheme) {
|
|
theme.value = null;
|
|
localStorage.removeItem("theme");
|
|
} else {
|
|
theme.value = "light";
|
|
localStorage.setItem("theme", theme.value);
|
|
}
|
|
changeTheme();
|
|
});
|
|
|
|
watch(theme, (newTheme) => {
|
|
console.debug("changing theme value", newTheme);
|
|
if (newTheme) {
|
|
localStorage.setItem("theme", newTheme);
|
|
}
|
|
changeTheme();
|
|
});
|
|
|
|
const changeTheme = () => {
|
|
console.debug("changing theme to apply");
|
|
if (
|
|
localStorage.getItem("theme") === "dark" ||
|
|
(!("theme" in localStorage) &&
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
|
) {
|
|
console.debug("applying dark theme");
|
|
document.documentElement.classList.add("dark");
|
|
} else {
|
|
console.debug("removing dark theme");
|
|
document.documentElement.classList.remove("dark");
|
|
}
|
|
};
|
|
|
|
const selectedTimezone = computed({
|
|
get() {
|
|
if (loggedUser.value?.settings?.timezone) {
|
|
return loggedUser.value.settings.timezone;
|
|
}
|
|
const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
if (loggedUser.value?.settings?.timezone === null) {
|
|
updateUserSettings({ timezone: detectedTimezone });
|
|
}
|
|
return detectedTimezone;
|
|
},
|
|
set(newSelectedTimezone: string) {
|
|
if (newSelectedTimezone !== loggedUser.value?.settings?.timezone) {
|
|
updateUserSettings({ timezone: newSelectedTimezone });
|
|
}
|
|
},
|
|
});
|
|
|
|
const sanitize = (timezone: string): string => {
|
|
return timezone
|
|
.split("_")
|
|
.join(" ")
|
|
.replace("St ", "St. ")
|
|
.split("/")
|
|
.join(" - ");
|
|
};
|
|
|
|
const timezones = computed((): Record<string, string[]> => {
|
|
if (!serverTimezones.value) return {};
|
|
return serverTimezones.value.reduce(
|
|
(acc: { [key: string]: Array<string> }, val: string) => {
|
|
const components = val.split("/");
|
|
const [prefix, suffix] = [
|
|
components.shift() as string,
|
|
components.join("/"),
|
|
];
|
|
const pushOrCreate = (
|
|
acc2: { [key: string]: Array<string> },
|
|
prefix2: string,
|
|
suffix2: string
|
|
) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
(acc2[prefix2] = acc2[prefix2] || []).push(suffix2);
|
|
return acc2;
|
|
};
|
|
if (suffix) {
|
|
return pushOrCreate(acc, prefix, suffix);
|
|
}
|
|
return pushOrCreate(acc, t("Other") as string, prefix);
|
|
},
|
|
{}
|
|
);
|
|
});
|
|
|
|
const address = computed({
|
|
get(): IAddress | null {
|
|
if (
|
|
loggedUser.value?.settings?.location?.name &&
|
|
loggedUser.value?.settings?.location?.geohash
|
|
) {
|
|
const { latitude, longitude } = ngeohash.decode(
|
|
loggedUser.value?.settings?.location?.geohash
|
|
);
|
|
const name = loggedUser.value?.settings?.location?.name;
|
|
return {
|
|
description: name,
|
|
locality: "",
|
|
type: "administrative",
|
|
geom: `${longitude};${latitude}`,
|
|
street: "",
|
|
postalCode: "",
|
|
region: "",
|
|
country: "",
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
set(newAddress: IAddress | null) {
|
|
if (newAddress?.geom) {
|
|
const { geom } = newAddress;
|
|
const addressObject = new Address(newAddress);
|
|
const queryText = addressObject.poiInfos.name;
|
|
const [lon, lat] = geom.split(";");
|
|
const geohash = ngeohash.encode(lat, lon, 6);
|
|
if (queryText && geom) {
|
|
updateUserSettings({
|
|
location: {
|
|
geohash,
|
|
name: queryText,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|
|
const locationRange = computed({
|
|
get(): number | undefined | null {
|
|
return loggedUser.value?.settings?.location?.range;
|
|
},
|
|
set(newLocationRange: number | undefined | null) {
|
|
if (newLocationRange) {
|
|
updateUserSettings({
|
|
location: {
|
|
range: newLocationRange,
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
const resetArea = (): void => {
|
|
updateUserSettings({
|
|
location: {
|
|
geohash: null,
|
|
name: null,
|
|
range: null,
|
|
},
|
|
});
|
|
};
|
|
|
|
const { mutate: updateUserSettings } = useMutation<{ setUserSetting: string }>(
|
|
SET_USER_SETTINGS,
|
|
() => ({
|
|
refetchQueries: [{ query: USER_SETTINGS }],
|
|
})
|
|
);
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.reset-area {
|
|
align-self: center;
|
|
position: relative;
|
|
top: 10px;
|
|
}
|
|
</style>
|