Skip to content

Commit c246f61

Browse files
committed
feat: add general settings dialog and move language and currency there
1 parent 3b4b7bd commit c246f61

File tree

16 files changed

+278
-195
lines changed

16 files changed

+278
-195
lines changed

src/app/components/base/dialog/Dialog.vue

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<dialog ref="dialog" :class="classes" @transitionend="transitionEnd">
3+
<div :class="$style.backdrop" />
34
<div ref="content" :class="[$style.content, contentClass]">
45
<h3 v-if="title" :class="$style.title">{{ title }}</h3>
56
<slot />
@@ -41,10 +42,11 @@ watch(
4142
[toRef(props, 'open'), dialog],
4243
() => {
4344
if (props.open) {
44-
dialog.value?.show();
45+
dialog.value?.showModal();
4546
requestAnimationFrame(() => (visible.value = true));
4647
} else {
4748
visible.value = false;
49+
requestAnimationFrame(() => dialog.value?.close());
4850
}
4951
},
5052
{ immediate: true }
@@ -60,6 +62,7 @@ onMounted(() => {
6062

6163
<style lang="scss" module>
6264
.dialog {
65+
visibility: hidden;
6366
position: fixed;
6467
inset: 0 0 0 0;
6568
display: flex;
@@ -74,26 +77,30 @@ onMounted(() => {
7477
z-index: 1;
7578
}
7679
77-
.dialog,
78-
.dialog[open]::backdrop {
79-
visibility: hidden;
80-
-webkit-backdrop-filter: blur(2px);
81-
backdrop-filter: blur(2px);
82-
opacity: 0;
83-
transition:
84-
visibility 0s var(--transition-m),
85-
opacity var(--transition-m);
80+
.backdrop {
81+
position: absolute;
82+
transition: all var(--transition-m);
83+
inset: 0 0 0 0;
84+
z-index: -1;
8685
}
8786
88-
.dialog[open].open,
89-
.dialog[open].open::backdrop {
90-
transition-delay: 0s;
87+
.dialog[open] {
88+
visibility: visible;
89+
90+
.content {
91+
transition: opacity var(--transition-m);
92+
}
9193
}
9294
93-
.dialog[open].open,
94-
.dialog[open].open::backdrop {
95-
opacity: 1;
96-
visibility: visible;
95+
.dialog[open].open {
96+
.content {
97+
opacity: 1;
98+
}
99+
100+
.backdrop {
101+
-webkit-backdrop-filter: blur(2px);
102+
backdrop-filter: blur(2px);
103+
}
97104
}
98105
99106
.content {
@@ -102,6 +109,7 @@ onMounted(() => {
102109
padding: 16px 18px;
103110
border-radius: var(--border-radius-l);
104111
box-shadow: var(--dialog-box-shadow);
112+
opacity: 0;
105113
}
106114
107115
.title {

src/app/components/base/form/TextField.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const passwordBarColor = computed(() => {
9797
border: 1px solid var(--input-field-border);
9898
border-radius: var(--border-radius-m);
9999
height: 30px;
100-
padding: 16px 12px;
100+
padding: 0 12px;
101101
display: flex;
102102
align-items: center;
103103
transition: all var(--input-field-transition);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<template>
2+
<div :class="$style.select">
3+
<label :for="fieldId" :class="$style.label">{{ label }}</label>
4+
5+
<ContextMenu tooltip-position="bottom" :options="options" position="bottom" @select="modelValue = $event.id">
6+
<template #default="{ toggle }">
7+
<button :id="fieldId" :class="$style.btn" type="button" @click="toggle">
8+
{{ currentValue }}
9+
</button>
10+
</template>
11+
</ContextMenu>
12+
</div>
13+
</template>
14+
15+
<script lang="ts" setup>
16+
import { ContextMenuOption, ContextMenuOptionId } from '@components/base/context-menu/ContextMenu.types.ts';
17+
import ContextMenu from '@components/base/context-menu/ContextMenu.vue';
18+
import { uuid } from '@utils';
19+
import { computed } from 'vue';
20+
21+
const modelValue = defineModel<ContextMenuOptionId>();
22+
23+
const props = defineProps<{
24+
label: string;
25+
options?: ContextMenuOption[];
26+
}>();
27+
28+
const fieldId = uuid();
29+
const currentValue = computed(
30+
() => props.options?.find((option) => option.id === modelValue.value)?.label ?? 'Select...'
31+
);
32+
</script>
33+
34+
<style lang="scss" module>
35+
.select {
36+
display: flex;
37+
flex-direction: column;
38+
gap: 5px;
39+
}
40+
41+
.label {
42+
font-weight: var(--font-weight-l);
43+
font-size: var(--font-size-xs);
44+
}
45+
46+
.btn {
47+
all: unset;
48+
cursor: pointer;
49+
background: var(--input-field-background);
50+
border: 1px solid var(--input-field-border);
51+
border-radius: var(--border-radius-m);
52+
height: 30px;
53+
width: 100%;
54+
padding: 0 12px;
55+
display: flex;
56+
align-items: center;
57+
transition: all var(--input-field-transition);
58+
font-size: var(--input-field-font-size);
59+
60+
&:hover:not(:focus-within) {
61+
border-color: var(--input-field-hover-border);
62+
background: var(--input-field-hover-background);
63+
}
64+
65+
&:focus-within {
66+
box-shadow: 0 0 0 1px inset var(--input-field-hover-border);
67+
border-color: var(--input-field-focus-border);
68+
background: var(--input-field-focus-background);
69+
}
70+
}
71+
</style>

src/app/pages/Frame.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@
2424
<ChangeYearButton :class="$style.btn" />
2525

2626
<template v-if="media !== 'mobile'">
27-
<ChangeLanguageButton :class="$style.btn" />
28-
<ChangeCurrencyButton :class="$style.btn" />
27+
<SettingsButton :class="$style.btn" />
2928
<InfoButton :class="$style.btn" />
3029
</template>
3130

@@ -52,12 +51,11 @@ import { computed, ref } from 'vue';
5251
import { useI18n } from 'vue-i18n';
5352
import AdminButton from './navigation/admin/AdminButton.vue';
5453
import CloudButton from './navigation/auth/CloudButton.vue';
55-
import ChangeCurrencyButton from './navigation/currency/ChangeCurrencyButton.vue';
5654
import InfoButton from './navigation/info/InfoButton.vue';
57-
import ChangeLanguageButton from './navigation/language/ChangeLanguageButton.vue';
5855
import ThemeButton from './navigation/theme/ThemeButton.vue';
5956
import ToolsButton from './navigation/tools/ToolsButton.vue';
6057
import ChangeYearButton from './navigation/year/ChangeYearButton.vue';
58+
import SettingsButton from './navigation/settings/SettingsButton.vue';
6159
import type { Component } from 'vue';
6260
6361
const menu = ref<HTMLDivElement>();

src/app/pages/navigation/currency/ChangeCurrencyButton.vue

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/app/pages/navigation/language/ChangeLanguageButton.vue

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<Button
3+
:tooltip="t('navigation.settings.settings')"
4+
:class="classes"
5+
textual
6+
color="dimmed"
7+
:icon="RiEqualizerLine"
8+
@click="showSettingsDialog = true"
9+
/>
10+
<SettingsDialog :open="showSettingsDialog" @close="showSettingsDialog = false" />
11+
</template>
12+
13+
<script lang="ts" setup>
14+
import Button from '@components/base/button/Button.vue';
15+
import { RiEqualizerLine } from '@remixicon/vue';
16+
import { ClassNames } from '@utils';
17+
import { computed, ref } from 'vue';
18+
import SettingsDialog from '@app/pages/navigation/settings/SettingsDialog.vue';
19+
import { useI18n } from 'vue-i18n';
20+
21+
const props = defineProps<{
22+
class?: ClassNames;
23+
}>();
24+
25+
const { t } = useI18n();
26+
const showSettingsDialog = ref(false);
27+
28+
const classes = computed(() => props.class);
29+
</script>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<template>
2+
<Dialog :open="open" :title="t('navigation.settings.settings')" @close="emit('close')">
3+
<div :class="$style.settingsDialog">
4+
<Select
5+
:model-value="state.locale"
6+
:label="t('navigation.settings.language')"
7+
:options="locales"
8+
@update:model-value="changeLocale($event as AvailableLocale)"
9+
/>
10+
11+
<Select
12+
:model-value="state.currency"
13+
:label="t('navigation.settings.currency')"
14+
:options="currencies"
15+
@update:model-value="changeCurrency($event as AvailableCurrency)"
16+
/>
17+
</div>
18+
</Dialog>
19+
</template>
20+
21+
<script lang="ts" setup>
22+
import Dialog from '@components/base/dialog/Dialog.vue';
23+
import { computed } from 'vue';
24+
import { useI18n } from 'vue-i18n';
25+
import { AvailableLocale, availableLocales, initialLocale } from '@i18n/index.ts';
26+
import { RiCheckLine } from '@remixicon/vue';
27+
import { useDataStore } from '@store/state';
28+
import { ContextMenuOption } from '@components/base/context-menu/ContextMenu.types.ts';
29+
import Select from '@components/base/select/Select.vue';
30+
import { availableCurrencies, AvailableCurrency } from '@store/state/types.ts';
31+
32+
const emit = defineEmits<{
33+
(e: 'close'): void;
34+
}>();
35+
36+
defineProps<{
37+
open: boolean;
38+
}>();
39+
40+
const { t, locale } = useI18n();
41+
const { changeCurrency, changeLocale, state } = useDataStore();
42+
43+
const locales = computed<ContextMenuOption[]>(() => {
44+
const displayNames = new Intl.DisplayNames(initialLocale, { type: 'language' });
45+
46+
return availableLocales.map((value) => ({
47+
id: value,
48+
icon: state.locale === value ? RiCheckLine : undefined,
49+
label: displayNames.of(value)
50+
}));
51+
});
52+
53+
const currencies = computed<ContextMenuOption[]>(() =>
54+
availableCurrencies.map((value) => ({
55+
id: value,
56+
icon: state.currency === value ? RiCheckLine : undefined,
57+
label: `${formatNumber(locale.value, value, 'name')} (${formatNumber(locale.value, value)})`
58+
}))
59+
);
60+
61+
const formatNumber = (locale: string, currency: string, currencyDisplay?: Intl.NumberFormatOptionsCurrencyDisplay) => {
62+
const symbol = new Intl.NumberFormat(locale, {
63+
style: 'currency',
64+
currencyDisplay,
65+
currency
66+
})
67+
.formatToParts(0)
68+
.find((x) => x.type === 'currency')?.value;
69+
70+
return symbol ? symbol[0].toUpperCase() + symbol.slice(1) : symbol;
71+
};
72+
</script>
73+
74+
<style lang="scss" module>
75+
.settingsDialog {
76+
width: 250px;
77+
display: flex;
78+
flex-direction: column;
79+
gap: 12px;
80+
}
81+
</style>

0 commit comments

Comments
 (0)