(ts) migrate some components to composition API with typescript

This commit is contained in:
Valentyne Stigloher 2025-02-25 12:57:29 +01:00
parent 8959e84889
commit a70de3d972
3 changed files with 330 additions and 368 deletions

View File

@ -1,3 +1,248 @@
<script setup lang="ts">
import { useCookie, useFetch } from 'nuxt/app';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import { newDate, gravatar } from '../src/helpers.ts';
import { socialProviders } from '../src/socialProviders.ts';
import { usernameRegex } from '../src/username.ts';
import { useMainStore } from '../store/index.ts';
import { getUrlForLocale, getUrlsForAllLocales } from '~/src/domain.ts';
import type { Profile } from '~/src/profile.ts';
const { $translator: translator, $setToken: setToken, $removeToken: removeToken } = useNuxtApp();
const runtimeConfig = useRuntimeConfig();
const config = useConfig();
const dialogue = useDialogue();
const { accounts, user } = storeToRefs(useMainStore());
if (user.value === null) {
throw 'no user';
}
const tokenCookie = useCookie('token', longtimeCookieSetting);
const impersonatorCookie = useCookie('impersonator');
const termsUpdateDismissed3Cookie = useCookie('termsUpdateDismissed3', longtimeCookieSetting);
const { data: profilesData } = useFetch(`/api/profile/get/${user.value.username}?version=2&props=hide`, {
lazy: true,
});
const { data: socialConnections } = useFetch('/api/user/social-connections', {
lazy: true,
});
const baseUrl = getUrlForLocale(config.locale);
const universalDomains = getUrlsForAllLocales(config.locale).filter((url) => url !== baseUrl);
const username = ref(user.value.username);
const email = ref(user.value.email);
const socialLookup = ref(Boolean(user.value.socialLookup));
const message = ref('');
const messageParams = ref({});
const messageIcon = ref<string | null>(null);
const error = ref('');
const changeEmailAuthId = ref(null);
const code = ref<string | null>('');
const savingUsername = ref(false);
const savingEmail = ref(false);
const showCaptcha = ref(false);
const captchaToken = ref(null);
const logoutInProgress = ref(false);
const showTermsUpdate = ref(newDate() < new Date(2023, 3, 6) && !termsUpdateDismissed3Cookie.value);
const profiles = computed(() => profilesData.value?.profiles);
const setProfiles = (profiles: Record<string, Partial<Profile>>) => {
profilesData.value.profiles = profiles;
};
const impersonationActive = computed(() => {
return !!impersonatorCookie.value;
});
const canChangeEmail = computed(() => {
return email.value && captchaToken.value;
});
const usernameError = computed(() => {
if (!username.value.match(usernameRegex)) {
return translator.translate('user.account.changeUsername.invalid');
}
if (username.value !== encodeURIComponent(username.value)) {
return translator.translate('user.account.changeUsername.nonascii', { encoded: encodeURIComponent(username.value) });
}
return null;
});
const hasEmail = computed(() => {
return user.value?.email && !user.value.email.endsWith('.oauth');
});
const authMethodsCount = computed(() => {
if (!user.value || socialConnections.value === null) {
return null;
}
return Object.keys(socialConnections.value).length + (hasEmail.value ? 1 : 0);
});
watch(email, (v) => {
if (v !== user.value?.email) {
showCaptcha.value = true;
}
});
watch(socialLookup, async (v) => {
const response = await dialogue.postWithAlertOnError<any>('/api/user/set-social-lookup', { socialLookup: v });
await setToken(response.token);
});
const router = useRouter();
onMounted(async () => {
const user = await $fetch('/api/user/current');
if (user) {
await setToken(user.token);
}
const redirectTo = window.sessionStorage.getItem('after-login');
if (user.value && redirectTo) {
window.sessionStorage.removeItem('after-login');
await router.push(redirectTo);
}
});
const changeUsername = async () => {
error.value = '';
if (savingUsername.value || !user.value) {
return;
}
savingUsername.value = true;
try {
const response = await dialogue.postWithAlertOnError<any>('/api/user/change-username', {
username: username.value,
});
if (response.error) {
error.value = response.error;
return;
}
await removeToken(user.value.username);
await setToken(response.token);
username.value = user.value.username;
message.value = 'crud.saved';
messageParams.value = {};
messageIcon.value = 'check-circle';
setTimeout(() => message.value = '', 3000);
} finally {
savingUsername.value = false;
}
};
const codeInput = useTemplateRef('codeInput');
const changeEmail = async () => {
error.value = '';
if (savingEmail.value) {
return;
}
savingEmail.value = true;
try {
const response = await dialogue.postWithAlertOnError<any>('/api/user/change-email', {
email: email.value,
authId: changeEmailAuthId.value,
code: code.value,
captchaToken: captchaToken.value,
});
if (response.error) {
error.value = response.error;
return;
}
if (!changeEmailAuthId.value) {
changeEmailAuthId.value = response.authId;
message.value = 'user.login.emailSent';
messageParams.value = { email: addBrackets(email.value) };
messageIcon.value = 'envelope-open-text';
await nextTick();
codeInput.value?.focus();
} else {
changeEmailAuthId.value = null;
message.value = '';
messageParams.value = {};
code.value = null;
await setToken(response.token);
message.value = 'crud.saved';
messageParams.value = {};
messageIcon.value = 'check-circle';
setTimeout(() => message.value = '', 3000);
}
} finally {
savingEmail.value = false;
}
};
const logout = () => {
logoutInProgress.value = true;
setTimeout(doLogout, 3000);
};
const logoutAll = () => {
window.localStorage.removeItem('account-tokens');
logout();
};
const doLogout = async () => {
await removeToken();
logoutInProgress.value = false;
setTimeout(() => window.location.reload(), 300);
};
const deleteAccount = async () => {
await dialogue.confirm(translator.translate('user.deleteAccountConfirm'), 'danger');
await dialogue.postWithAlertOnError('/api/user/delete');
if (impersonationActive.value) {
stopImpersonation();
} else {
logout();
}
};
const setAvatar = async (source: string | null) => {
const response = await dialogue.postWithAlertOnError<any>('/api/user/set-avatar', { source });
await setToken(response.token);
};
const uploaded = async (ids: string[]) => {
await setAvatar(`${runtimeConfig.public.cloudfront}/images/${ids[0]}-avatar.png`);
};
const stopImpersonation = async () => {
if (!user.value) {
return;
}
await removeToken(user.value.username);
tokenCookie.value = impersonatorCookie.value;
impersonatorCookie.value = null;
setTimeout(() => window.location.reload(), 300);
};
const dismissTermsUpdate = () => {
termsUpdateDismissed3Cookie.value = 'true';
showTermsUpdate.value = false;
};
const addBrackets = (str: string): string => {
return str ? `(${str})` : '';
};
</script>
<template>
<section v-if="logoutInProgress || !user">
<p class="text-center">
@ -150,7 +395,7 @@
</div>
<div v-else class="input-group mb-3">
<input
ref="code"
ref="codeInput"
v-model="code"
type="text"
class="form-control text-center"
@ -336,266 +581,6 @@
</section>
</template>
<script lang="ts">
import { useCookie, useFetch } from 'nuxt/app';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import { newDate, gravatar } from '../src/helpers.ts';
import { socialProviders } from '../src/socialProviders.ts';
import { usernameRegex } from '../src/username.ts';
import { useMainStore } from '../store/index.ts';
import { getUrlForLocale, getUrlsForAllLocales } from '~/src/domain.ts';
import type { Profile } from '~/src/profile.ts';
export default defineComponent({
async setup() {
const dialogue = useDialogue();
const { accounts, user } = storeToRefs(useMainStore());
if (user.value === null) {
throw 'no user';
}
const tokenCookie = useCookie('token', longtimeCookieSetting);
const impersonatorCookie = useCookie('impersonator');
const termsUpdateDismissed3Cookie = useCookie('termsUpdateDismissed3', longtimeCookieSetting);
const { data: profilesData } = useFetch(`/api/profile/get/${user.value.username}?version=2&props=hide`, {
lazy: true,
});
const { data: socialConnections } = useFetch('/api/user/social-connections', {
lazy: true,
});
const config = useConfig();
const baseUrl = getUrlForLocale(config.locale);
return {
config,
universalDomains: getUrlsForAllLocales(config.locale).filter((url) => url !== baseUrl),
dialogue,
user,
username: ref(user.value.username),
email: ref(user.value.email),
socialLookup: ref(Boolean(user.value.socialLookup)),
accounts,
tokenCookie,
impersonatorCookie,
termsUpdateDismissed3Cookie,
profilesData,
socialConnections,
};
},
data() {
return {
message: '',
messageParams: {},
messageIcon: null as string | null,
error: '',
changeEmailAuthId: null,
code: '' as string | null,
socialProviders,
savingUsername: false,
savingEmail: false,
gravatar,
showCaptcha: false,
captchaToken: null,
logoutInProgress: false,
showTermsUpdate: newDate() < new Date(2023, 3, 6) &&
!this.termsUpdateDismissed3Cookie,
};
},
computed: {
profiles() {
return this.profilesData?.profiles;
},
impersonationActive() {
return !!this.impersonatorCookie;
},
canChangeEmail() {
return this.email && this.captchaToken;
},
usernameError() {
if (!this.username.match(usernameRegex)) {
return this.$t('user.account.changeUsername.invalid');
}
if (this.username !== encodeURIComponent(this.username)) {
return this.$t('user.account.changeUsername.nonascii', { encoded: encodeURIComponent(this.username) });
}
return null;
},
hasEmail() {
return this.user?.email && !this.user.email.endsWith('.oauth');
},
authMethodsCount() {
if (!this.user || this.socialConnections === null) {
return null;
}
return Object.keys(this.socialConnections).length + (this.hasEmail ? 1 : 0);
},
},
watch: {
email(v) {
if (v !== this.user?.email) {
this.showCaptcha = true;
}
},
async socialLookup(v) {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-social-lookup', { socialLookup: v });
await this.$setToken(response.token);
},
},
async mounted() {
const user = await $fetch('/api/user/current');
if (user) {
await this.$setToken(user.token);
}
const redirectTo = window.sessionStorage.getItem('after-login');
if (this.user && redirectTo) {
window.sessionStorage.removeItem('after-login');
await this.$router.push(redirectTo);
}
},
methods: {
async changeUsername() {
this.error = '';
if (this.savingUsername || !this.user) {
return;
}
this.savingUsername = true;
try {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/change-username', {
username: this.username,
});
if (response.error) {
this.error = response.error;
return;
}
await this.$removeToken(this.user.username);
await this.$setToken(response.token);
this.username = this.user.username;
this.message = 'crud.saved';
this.messageParams = {};
this.messageIcon = 'check-circle';
setTimeout(() => this.message = '', 3000);
} finally {
this.savingUsername = false;
}
},
async changeEmail() {
this.error = '';
if (this.savingEmail) {
return;
}
this.savingEmail = true;
try {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/change-email', {
email: this.email,
authId: this.changeEmailAuthId,
code: this.code,
captchaToken: this.captchaToken,
});
if (response.error) {
this.error = response.error;
return;
}
if (!this.changeEmailAuthId) {
this.changeEmailAuthId = response.authId;
this.message = 'user.login.emailSent';
this.messageParams = { email: this.addBrackets(this.email) };
this.messageIcon = 'envelope-open-text';
this.$nextTick(() => {
(this.$refs.code as HTMLInputElement).focus();
});
} else {
this.changeEmailAuthId = null;
this.message = '';
this.messageParams = {};
this.code = null;
await this.$setToken(response.token);
this.message = 'crud.saved';
this.messageParams = {};
this.messageIcon = 'check-circle';
setTimeout(() => this.message = '', 3000);
}
} finally {
this.savingEmail = false;
}
},
logout() {
this.logoutInProgress = true;
setTimeout(this.doLogout, 3000);
},
logoutAll() {
window.localStorage.removeItem('account-tokens');
this.logout();
},
async doLogout() {
await this.$removeToken();
this.logoutInProgress = false;
setTimeout(() => window.location.reload(), 300);
},
setProfiles(profiles: Record<string, Partial<Profile>>) {
this.profiles = profiles;
},
async deleteAccount() {
await this.dialogue.confirm(this.$t('user.deleteAccountConfirm'), 'danger');
await this.dialogue.postWithAlertOnError('/api/user/delete');
if (this.impersonationActive) {
this.stopImpersonation();
} else {
this.logout();
}
},
async setAvatar(source: string | null) {
const response = await this.dialogue.postWithAlertOnError<any>('/api/user/set-avatar', { source });
await this.$setToken(response.token);
},
async uploaded(ids: string[]) {
await this.setAvatar(`${this.$config.public.cloudfront}/images/${ids[0]}-avatar.png`);
},
async stopImpersonation() {
if (!this.user) {
return;
}
await this.$removeToken(this.user.username);
this.tokenCookie = this.impersonatorCookie;
this.impersonatorCookie = null;
setTimeout(() => window.location.reload(), 300);
},
dismissTermsUpdate() {
this.termsUpdateDismissed3Cookie = 'true';
this.showTermsUpdate = false;
},
addBrackets(str: string): string {
return str ? `(${str})` : '';
},
},
});
</script>
<style lang="scss" scoped>
@import "assets/variables";

View File

@ -1,5 +1,33 @@
<script setup lang="ts">
import useConfig from '~/composables/useConfig.ts';
import type { Day, Year } from '~/src/calendar/helpers.ts';
import { socialLinks } from '~/src/contact.ts';
import { getUrlForLocale } from '~/src/domain.js';
import { clearUrl, newDate } from '~/src/helpers.ts';
const props = defineProps<{
day?: Day;
year?: Year;
}>();
const config = useConfig();
const bots = [
socialLinks.calendar?.mastodon,
socialLinks.calendar?.bluesky,
].filter((bot) => bot !== undefined);
const icsLink = computed(() => {
if (!props.year) {
return undefined;
}
const yearSuffix = props.year.year === newDate().getFullYear() ? '' : `-${props.year.year}`;
return `${getUrlForLocale(config.locale)}/api/queer-calendar-${config.locale}${yearSuffix}.ics`;
});
</script>
<template>
<section>
<section v-if="config.calendar?.enabled">
<div class="alert alert-info d-flex flex-column flex-md-row justify-content-around">
<div v-if="day" class="px-1">
<p class="h5">
@ -37,7 +65,7 @@
<T>calendar.image.header</T>
</a>
</p>
<p v-else class="mb-0">
<p v-else-if="year" class="mb-0">
<a
:href="`/calendar/${config.locale}/${year.year}-overview.png`"
target="_blank"
@ -74,38 +102,3 @@
</section>
</section>
</template>
<script>
import useConfig from '../composables/useConfig.ts';
import { socialLinks } from '../src/contact.ts';
import { clearUrl, newDate } from '../src/helpers.ts';
import { getUrlForLocale } from '~/src/domain.js';
export default {
props: {
day: {},
year: {},
},
setup() {
return {
config: useConfig(),
};
},
data() {
return {
bots: [
socialLinks.calendar.mastodon,
socialLinks.calendar.bluesky,
],
clearUrl,
};
},
computed: {
icsLink() {
const yearSuffix = this.year.year === newDate().getFullYear() ? '' : `-${this.year.year}`;
return `${getUrlForLocale(this.config.locale)}/api/queer-calendar-${this.config.locale}${yearSuffix}.ics`;
},
},
};
</script>

View File

@ -1,3 +1,57 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import logoSvg from '~/public/logo/logo.svg?raw';
import { Day } from '~/src/calendar/helpers.ts';
import { loadCalendar } from '~/src/data.ts';
import { ImmutableArray } from '~/src/helpers.ts';
import { useMainStore } from '~/store/index.ts';
const props = withDefaults(defineProps<{
flag?: boolean;
forceShowFlag?: boolean;
day?: Day;
}>(), {
day: () => Day.today(),
});
const selectedDay = storeToRefs(useMainStore()).selectedDay;
const calendar = await loadCalendar();
const svg = ref(logoSvg.replace('/></svg>', 'fill="currentColor"/></svg>'));
const flagName = ref<string | null>(null);
const forceShowFlagDyn = ref(false);
const selectFlag = (): string | null => {
const events = calendar.getCurrentYear()!.eventsByDate[(selectedDay.value || props.day).toString()];
if (!events) {
return null;
}
return new ImmutableArray(...events)
.filter((e) => e.display.type === 'flag' && !e.display.name.startsWith('_'))
.sorted((a, b) => b.level - a.level)
.groupBy((e) => e.level)
.indexOrFallback(0, ['0', new ImmutableArray()])[1]
.map((e) => e.display.name)
.randomElement();
};
watch(selectedDay, () => {
forceShowFlagDyn.value = !!selectedDay.value;
// removing the flag from the selected day is deferred until the transition has finished
// so that it does not suddenly change
if (selectedDay.value !== null) {
flagName.value = selectFlag();
}
});
const resetFlagIfNotOverwritten = (): void => {
if (selectedDay.value === null) {
flagName.value = selectFlag();
}
};
</script>
<template>
<span
v-if="flag"
@ -10,76 +64,6 @@
<span v-else :class="['logo', $attrs.class]" v-html="svg"></span>
</template>
<script lang="ts">
import { storeToRefs } from 'pinia';
import { defineComponent } from 'vue';
import logoSvg from '../public/logo/logo.svg?raw';
import { Day } from '../src/calendar/helpers.ts';
import { loadCalendar } from '../src/data.ts';
import { ImmutableArray } from '../src/helpers.ts';
import { useMainStore } from '../store/index.ts';
interface Data {
svg: string;
flagName: string | null;
forceShowFlagDyn: boolean;
}
export default defineComponent({
props: {
flag: { type: Boolean },
forceShowFlag: { type: Boolean },
day: { default: () => Day.today(), type: Day },
},
async setup() {
const calendarPromise = loadCalendar();
return {
selectedDay: storeToRefs(useMainStore()).selectedDay,
calendar: await calendarPromise,
};
},
data(): Data {
return {
svg: logoSvg.replace('/></svg>', 'fill="currentColor"/></svg>'),
flagName: null,
forceShowFlagDyn: false,
};
},
watch: {
selectedDay() {
this.forceShowFlagDyn = !!this.selectedDay;
// removing the flag from the selected day is deferred until the transition has finished
// so that it does not suddenly change
if (this.selectedDay !== null) {
this.flagName = this.selectFlag();
}
},
},
methods: {
selectFlag(): string | null {
const events = this.calendar.getCurrentYear()!.eventsByDate[(this.selectedDay || this.day).toString()];
if (!events) {
return null;
}
return new ImmutableArray(...events)
.filter((e) => e.display.type === 'flag' && !e.display.name.startsWith('_'))
.sorted((a, b) => b.level - a.level)
.groupBy((e) => e.level)
.indexOrFallback(0, ['0', new ImmutableArray()])[1]
.map((e) => e.display.name)
.randomElement();
},
resetFlagIfNotOverwritten(): void {
if (this.selectedDay === null) {
this.flagName = this.selectFlag();
}
},
},
});
</script>
<style lang="scss">
.logo-wrapper {
width: 1.3em;