Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: revamp profile pages #145

Merged
merged 1 commit into from
Sep 27, 2023
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
293 changes: 27 additions & 266 deletions components/account/CountrySelect.vue
Original file line number Diff line number Diff line change
@@ -1,291 +1,52 @@
<script setup lang="ts">
import { debouncedWatch, get, set } from '@vueuse/core';
import { get } from '@vueuse/core';
import { type Country } from '~/composables/countries';
import { type BaseErrorObject } from '~/types/common';
const props = withDefaults(
defineProps<{
modelValue: string;
id: string;
label: string;
countries?: Country[];
hint?: string;
disabled?: boolean;
errorMessages?: BaseErrorObject[];
autocomplete?: string;
}>(),
{
countries: () => [],
hint: '',
disabled: false,
errorMessages: () => [],
autocomplete: undefined,
},
);
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'blur'): void;
(e: 'selected', country: Country | null): void;
}>();
const { countries, modelValue: value, disabled } = toRefs(props);
const selected = ref<Country | null>(null);
const optionsShown = ref(false);
const searchFilter = ref('');
const { modelValue } = toRefs(props);
const filteredOptions = computed(() => {
const filtered: Country[] = [];
const filter = get(searchFilter);
const regOption = new RegExp(filter, 'ig');
for (const option of get(countries)) {
if (filter.length === 0 || option.name.match(regOption)) {
filtered.push(option);
}
}
return filtered;
});
const selectOption = (option: Country) => {
set(selected, option);
set(optionsShown, false);
set(searchFilter, option.name);
emit('update:modelValue', option.code);
};
const showOptions = () => {
if (!get(disabled)) {
set(searchFilter, '');
set(optionsShown, true);
}
};
const { t } = useI18n();
const exit = () => {
const filtered = get(filteredOptions);
if (filtered.length > 0 && get(searchFilter)) {
selectOption(filtered[0]);
} else {
const selection = get(selected);
if (selection) {
selectOption(selection);
} else {
set(searchFilter, '');
}
}
set(optionsShown, false);
emit('blur');
};
const { countries } = useCountries();
const keyMonitor = (event: KeyboardEvent) => {
const filtered = get(filteredOptions);
if (event.key === 'Enter' && filtered[0]) {
selectOption(filtered[0]);
}
};
const getFlagEmoji = (code: string) => {
const codePoints = code
.toUpperCase()
.split('')
.map((char) => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
};
const isEmpty = computed(() => {
const filter = get(searchFilter);
const selection = get(selected);
return !filter || selection?.name !== filter;
});
watch(selected, (newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
emit('update:modelValue', newValue?.code ?? '');
}
});
onMounted(() => setCountryUsingCode(get(value)));
function setCountryUsingCode(value: string) {
const cnt = get(countries);
const country = cnt.find(({ code }) => code === value);
if (country && country !== get(selected)) {
selectOption(country);
}
}
debouncedWatch(
value,
(value) => {
if (!value) {
set(selected, null);
} else {
setCountryUsingCode(value);
}
const country = computed({
get() {
return get(countries).find(({ code }) => code === get(modelValue)) || null;
},
{ debounce: 800 },
);
emit('selected', get(selected));
const css = useCssModule();
set(item: Country | null) {
emit('update:modelValue', item ? item.code : '');
},
});
</script>

<template>
<div :class="css.wrapper">
<div>
<div :class="css.inputContainer">
<div v-if="countries" :class="css.dropdown">
<span
v-if="selected && searchFilter === selected.name"
:class="css.flag"
>
{{ getFlagEmoji(selected.code) }}</span
>
<input
:id="id"
v-model="searchFilter"
:name="id"
autocomplete="off"
:class="{
[css.input]: true,
[css.empty]: isEmpty,
}"
:disabled="disabled"
@focus="showOptions()"
@blur="exit()"
@keyup="keyMonitor($event)"
/>
<label
v-if="label"
:class="{
[css.label]: true,
[css.select]: true,
}"
:for="id"
>
{{ label }}
</label>

<div v-show="optionsShown" :class="css.content">
<div
v-for="(option, index) in filteredOptions"
:key="index"
:class="css.item"
@mousedown="selectOption(option)"
>
{{ getFlagEmoji(option.code) }}
{{ option.name || '-' }}
</div>
</div>
</div>
</div>
</div>

<span
v-if="errorMessages && errorMessages.length > 0"
:id="`${id}-error`"
:class="css.error"
>
{{ errorMessages[0].$message }}
</span>

<span v-else :class="css.caption">
<slot name="hint">{{ hint }}</slot>
</span>
<slot />
</div>
<RuiAutoComplete
id="country"
v-model="country"
variant="outlined"
color="primary"
autocomplete="country"
:data="countries"
:disabled="disabled"
key-prop="code"
text-prop="name"
:label="t('auth.signup.address.form.country')"
>
<template #no-data>
{{ t('country_select.no_data') }}
</template>
</RuiAutoComplete>
</template>

<style lang="scss" module>
.wrapper {
@apply flex flex-col pt-2.5;
}
.inputContainer {
@apply relative w-full;
}
.input {
@apply block border-rui-grey-300 box-border border-solid focus:outline-none focus:border-rui-primary pr-2 px-3 appearance-none w-full bg-white;
margin-top: 8px;
border-width: 1px;
border-radius: 8px;
height: 56px;
& ~ label {
@apply top-4;
transform-origin: 0 0;
left: 26px;
}
&:focus-within ~ label {
@apply transform scale-75 -translate-y-4 text-rui-primary-darker duration-300;
}
&:not(.empty) {
@apply pl-9;
}
&:not(.empty):not(:focus-within) ~ label {
@apply transform scale-75 -translate-y-4 text-rui-text-secondary duration-300;
}
&:disabled {
@apply bg-gray-200;
}
}
.label {
@apply absolute top-4 text-rui-text-secondary text-sm;
}
.dropdown {
@apply relative block m-auto;
.dropdown:hover .content {
@apply block;
}
}
.content {
@apply absolute bg-white max-h-60 border border-solid border-rui-grey-200 shadow-md overflow-auto z-10;
min-width: 280px;
max-width: 280px;
}
.item {
@apply text-base text-rui-grey-800 p-2 block cursor-pointer;
text-decoration: none;
&:hover {
background-color: #e7ecf5;
}
}
%text-style {
@apply text-xs font-sans;
color: #808080;
margin-top: 8px;
}
.caption {
@extend %text-style;
}
.error {
color: #e53935;
@extend %text-style;
}
.empty {
@apply pl-3;
}
.flag {
@apply absolute top-4 left-3;
}
</style>
2 changes: 1 addition & 1 deletion components/account/activation/AccountActivate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const { t } = useI18n();
<span>
{{ t('auth.activation.success.message') }}
</span>
<ButtonLink to="/home" inline color="primary">
<ButtonLink to="/home/subscription" inline color="primary">
{{ t('common.here') }}
</ButtonLink>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/account/activation/PendingActivation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { t } = useI18n();
<div
class="container h-full flex flex-col pt-10 md:pt-0 md:flex-1 md:items-center md:justify-center"
>
<div class="max-w-[30.6rem] max-w-full space-y-3">
<div class="w-[30.6rem] max-w-full space-y-3">
<div class="text-h4">{{ t('auth.pending_activation.title') }}</div>
<div class="text-body-1 text-rui-text-secondary">
{{ t('auth.pending_activation.message') }}
Expand Down
23 changes: 14 additions & 9 deletions components/account/delete/AccountDeletedPage.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<script setup lang="ts">
const { t } = useI18n();
</script>

<template>
<PageContainer>
<template #title> Account Delete </template>
<PageContent>
<UserActionMessage>
<template #header>Your account was deleted</template>
<p>Your rotki premium account has been successfully deleted.</p>
</UserActionMessage>
</PageContent>
</PageContainer>
<div
class="container h-full flex flex-col pt-10 md:pt-0 md:flex-1 md:items-center md:justify-center"
>
<div class="max-w-[380px] max-w-full space-y-3">
<div class="text-h4">{{ t('account.account_deleted.title') }}</div>
<div class="text-body-1 text-rui-text-secondary">
{{ t('account.account_deleted.message') }}
</div>
</div>
</div>
</template>
Loading