(ts) migrate some components to composition API with typescript

This commit is contained in:
Valentyne Stigloher 2024-09-20 20:52:39 +02:00
parent d61f5c306b
commit 9516a93501
13 changed files with 300 additions and 351 deletions

View File

@ -219,7 +219,7 @@
</template>
<script>
import forbidden from '../src/forbidden.js';
import forbidden from '../src/forbidden.ts';
import { sleep } from '../src/helpers.ts';
import useDialogue from '../composables/useDialogue.ts';

View File

@ -1,5 +1,57 @@
<script setup lang="ts">
import Columnist from 'avris-columnist';
import type { Post } from '~/server/express/blog.ts';
import useConfig from '../composables/useConfig.ts';
const props = defineProps<{
posts: string[] | Post[];
details?: boolean;
}>();
const { data: postsFull } = useAsyncData(`posts-${JSON.stringify(props.posts)}`, async () => {
if (!props.posts.length) {
return [];
}
if (typeof props.posts[0] === 'object') {
return props.posts;
}
return await $fetch('/api/blog', {
params: {
slugs: props.posts,
},
});
});
const config = useConfig();
const shortcuts: Record<string, string | undefined> = {};
if (config.blog && config.blog.shortcuts) {
for (const shortcut in config.blog.shortcuts) {
if (!config.blog.shortcuts.hasOwnProperty(shortcut)) {
continue;
}
shortcuts[config.blog.shortcuts[shortcut]] = shortcut;
}
}
const generateLink = (slug: string): string => {
const keepFullPath = config.blog?.keepFullPath || [];
return shortcuts[slug] !== undefined && !keepFullPath.includes(slug)
? `/${shortcuts[slug]}`
: `/${config.links.blogRoute}/${slug}`;
};
const entries = useTemplateRef<HTMLDivElement>('entries');
onMounted(async () => {
if (entries.value) {
const columnist = new Columnist(entries.value);
columnist.start();
}
});
</script>
<template>
<div class="columnist-wall row">
<div ref="entries" class="columnist-wall row">
<div v-for="post in postsFull" class="columnist-column col-12 col-sm-6 col-md-4 mb-3">
<div class="card shadow">
<nuxt-link v-if="post.hero" :to="generateLink(post.slug)">
@ -30,64 +82,6 @@
</div>
</template>
<script>
import Columnist from 'avris-columnist';
import useConfig from '../composables/useConfig.ts';
export default {
props: {
posts: { required: true },
details: { type: Boolean },
},
setup(props) {
const { data: postsFull } = useAsyncData(`posts-${JSON.stringify(props.posts)}`, async () => {
if (!props.posts.length) {
return [];
}
if (typeof props.posts[0] === 'object') {
return props.posts;
}
return await $fetch('/api/blog', {
params: {
slugs: props.posts,
},
});
});
return {
config: useConfig(),
postsFull,
};
},
data() {
const shortcuts = {};
if (this.config.blog && this.config.blog.shortcuts) {
for (const shortcut in this.config.blog.shortcuts) {
if (!this.config.blog.shortcuts.hasOwnProperty(shortcut)) {
continue;
}
shortcuts[this.config.blog.shortcuts[shortcut]] = shortcut;
}
}
return {
shortcuts,
};
},
async mounted() {
const columnist = new Columnist(this.$el);
columnist.start();
},
methods: {
generateLink(slug) {
return this.shortcuts[slug] !== undefined && !(this.config.blog.keepFullPath || []).includes(slug)
? `/${this.shortcuts[slug]}`
: `/${this.config.links.blogRoute}/${slug}`;
},
},
};
</script>
<style lang="scss" scoped>
.columnist-wall > .columnist-column {
transition: margin-top .2s ease-in-out;

View File

@ -1,3 +1,12 @@
<script setup lang="ts">
import { useMainStore } from '../store/index.ts';
const config = useConfig();
const store = useMainStore();
const darkMode = store.darkMode;
</script>
<template>
<Submenu
v-if="config.community"
@ -44,22 +53,3 @@
}]"
/>
</template>
<script>
import { mapState } from 'pinia';
import useConfig from '../composables/useConfig.ts';
import { useMainStore } from '../store/index.ts';
export default {
setup() {
return {
config: useConfig(),
};
},
computed: {
...mapState(useMainStore, [
'darkMode',
]),
},
};
</script>

View File

@ -185,14 +185,14 @@
<!--<div class="container">
<SafariWarning dismissable />
</div>-->
<div v-if="$user() && $user().bannedReason" class="alert alert-danger mb-0 container">
<div v-if="$user() && $user()!.bannedReason" class="alert alert-danger mb-0 container">
<p class="h4 mb-2">
<Icon v="ban" />
<T>ban.header</T>
</p>
<p>
<T>ban.reason</T><T>quotation.colon</T>
{{ $user().bannedReason }}
{{ $user()!.bannedReason }}
</p>
<p>
<T>ban.termsIntro</T><T>quotation.colon</T>
@ -200,7 +200,7 @@
<blockquote class="small">
<T>terms.content.content.violations</T>
<template v-for="(violation, i) in forbidden">
<T :class="[$user().bannedTerms.includes(violation) ? 'fw-bold' : '']">terms.content.content.violationsExamples.{{ violation }}</T><template v-if="i !== forbidden.length - 1">
<T :class="[$user()!.bannedTerms?.includes(violation) ? 'fw-bold' : '']">terms.content.content.violationsExamples.{{ violation }}</T><template v-if="i !== forbidden.length - 1">
,
</template>
</template>.
@ -218,19 +218,34 @@
</header>
</template>
<script>
<script lang="ts">
import { mapState } from 'pinia';
import { DateTime } from 'luxon';
import forbidden from '../src/forbidden.js';
import forbidden from '../src/forbidden.ts';
import NounsNav from '../data/nouns/NounsNav.vue';
import useConfig from '../composables/useConfig.ts';
import { useMainStore } from '../store/index.ts';
import type { User } from '../src/user.ts';
export default {
interface HeaderLink {
header?: boolean;
link: string;
avatar?: User | null;
icon: string;
text: string;
textLong?: string;
extra?: (string | null)[];
desktop?: boolean;
mobile?: boolean;
}
export default defineComponent({
components: { NounsNav },
setup() {
const hoverItem = ref<HeaderLink | null>(null);
return {
config: useConfig(),
hoverItem,
};
},
data() {
@ -239,7 +254,6 @@ export default {
hamburgerShown: false,
censusDismissed: false,
forbidden,
hoverItem: null,
};
},
computed: {
@ -247,8 +261,8 @@ export default {
'user',
'darkMode',
]),
links() {
const links = [];
links(): HeaderLink[] {
const links: HeaderLink[] = [];
links.push({
header: true,
@ -351,14 +365,14 @@ export default {
this.config.contact && this.config.contact.team && this.config.contact.team.enabled
) {
const extra = [
this.config.terminology && this.config.terminology.enabled && this.config.terminology.published ? `/${this.config.terminology.route}` : '',
this.config.calendar && this.config.calendar.enabled ? `/${this.config.calendar.route}` : '',
this.config.census && this.config.census.enabled ? `/${this.config.census.route}` : '',
this.config.inclusive && this.config.inclusive.enabled ? `/${this.config.inclusive.route}` : '',
this.config.names && this.config.names.enabled && this.config.names.published ? `/${this.config.names.route}` : '',
this.config.people && this.config.people.enabled ? `/${this.config.people.route}` : '',
`/${this.config.contact.team.route}`,
this.config.workshops && this.config.workshops.enabled ? `/${this.config.workshops.route}` : '',
this.config.terminology.enabled && this.config.terminology.published ? `/${this.config.terminology.route}` : '',
this.config.calendar?.enabled ? `/${this.config.calendar.route}` : '',
this.config.census.enabled ? `/${this.config.census.route}` : '',
this.config.inclusive.enabled ? `/${this.config.inclusive.route}` : '',
this.config.names.enabled && this.config.names.published ? `/${this.config.names.route}` : '',
this.config.people.enabled ? `/${this.config.people.route}` : '',
this.config.contact.enabled ? `/${this.config.contact.team.route}` : '',
this.config.workshops?.enabled ? `/${this.config.workshops.route}` : '',
];
if (this.config.community) {
@ -396,13 +410,14 @@ export default {
});
if (this.config.user.enabled) {
const user = this.$user();
links.push({
link: `/${this.config.user.route}`,
avatar: this.$user(),
icon: 'user',
text: this.user ? `@${this.user.username}` : this.$t('user.header'),
textLong: this.user ? `@${this.user.username}` : this.$t('user.headerLong'),
extra: ['/editor', this.$user() ? `/@${this.$user().username}` : null],
extra: ['/editor', user ? `/@${user.username}` : null],
});
if (this.$isGranted('panel')) {
links.push({
@ -426,12 +441,12 @@ export default {
return links;
},
showCensus() {
showCensus(): boolean {
if (!process.client) {
return false;
}
const finished = !!parseInt(window.localStorage.getItem(`census-${this.config.census.edition}-finished`) || 0);
const dismissed = !!parseInt(window.localStorage.getItem(`census-${this.config.census.edition}-dismissed`) || 0);
const finished = !!parseInt(window.localStorage.getItem(`census-${this.config.census.edition}-finished`) || '0');
const dismissed = !!parseInt(window.localStorage.getItem(`census-${this.config.census.edition}-dismissed`) || '0');
const alreadyIn = this.$route.path === `/${this.config.census.route}`;
const isHomepage = this.$route.path === '/';
if (!this.config.census.enabled || !isHomepage && (finished || dismissed) || this.censusDismissed || alreadyIn) {
@ -439,7 +454,7 @@ export default {
}
const start = DateTime.fromISO(this.config.census.start).toLocal();
const end = DateTime.fromISO(this.config.census.end).toLocal();
const now = DateTime.utc().setZone(this.config.format.timezone);
const now = DateTime.utc().setZone(this.config.format?.timezone ?? 'utc');
return now >= start && now <= end;
},
},
@ -453,29 +468,29 @@ export default {
document.removeEventListener('scroll', this.updateShown);
},
methods: {
isActiveRoute(link) {
isActiveRoute(link: HeaderLink) {
return decodeURIComponent(this.$route.path) === link.link ||
link.extra && this.$route.name && link.extra.includes(this.$route.name.split(':')[0]) ||
link.extra && this.$route.name && link.extra.includes((this.$route.name as string).split(':')[0]) ||
(link.extra || []).includes(decodeURIComponent(this.$route.path)) ||
(link.extra || []).filter((x) => x && (
decodeURIComponent(this.$route.path).startsWith(`${x}/`) ||
decodeURIComponent(this.$route.path).startsWith(`${x}:`))).length;
},
documentClicked() {
documentClicked(): void {
if (this.hamburgerActive) {
this.hamburgerActive = false;
}
},
updateShown() {
const st = document.body.scrollTop || document.querySelector('html').scrollTop;
updateShown(): void {
const st = document.body.scrollTop || document.querySelector('html')!.scrollTop;
this.hamburgerShown = st > 300;
},
dismissCensus() {
dismissCensus(): void {
window.localStorage.setItem(`census-${this.config.census.edition}-dismissed`, '1');
this.censusDismissed = true;
},
},
};
});
</script>
<style lang="scss" scoped>

View File

@ -1,6 +1,15 @@
<script setup lang="ts">
import { useMainStore } from '../store/index.ts';
const config = useConfig();
const store = useMainStore();
const darkMode = store.darkMode;
</script>
<template>
<Submenu
v-if="config.links.split"
v-if="config.links.enabled && config.links.split"
:links="[{
name: 'links.links',
route: config.links.route,
@ -40,22 +49,3 @@
}]"
/>
</template>
<script>
import { mapState } from 'pinia';
import useConfig from '../composables/useConfig.ts';
import { useMainStore } from '../store/index.ts';
export default {
setup() {
return {
config: useConfig(),
};
},
computed: {
...mapState(useMainStore, [
'darkMode',
]),
},
};
</script>

View File

@ -1,3 +1,15 @@
<script setup lang="ts">
defineProps<{
link: string;
locale: string;
}>();
const config = useConfig();
const runtimeConfig = useRuntimeConfig();
const homeLink = runtimeConfig.public.homeUrl;
</script>
<template>
<nuxt-link v-if="locale === config.locale" :to="link" :class="$attrs.class">
<slot></slot>
@ -9,24 +21,3 @@
<slot></slot>
</a>
</template>
<script>
import useConfig from '../composables/useConfig.ts';
export default {
props: {
link: { required: true },
locale: { required: true },
},
setup() {
return {
config: useConfig(),
};
},
data() {
return {
homeLink: this.$config.public.home,
};
},
};
</script>

View File

@ -1,19 +1,16 @@
<template>
<a v-if="isExternal" :href="to" target="_blank" rel="noopener" :class="$attrs.class"><slot></slot></a>
<nuxt-link v-else :to="to">
<slot></slot>
</nuxt-link>
</template>
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router';
<script>
export default {
props: {
to: { required: true, type: String },
},
computed: {
isExternal() {
return this.to.startsWith('https://') || this.to.startsWith('http://');
},
},
};
const props = defineProps<{
to: RouteLocationRaw,
}>();
const isExternal = computed((): boolean => {
return typeof props.to === 'string' && (props.to.startsWith('https://') || props.to.startsWith('http://'));
});
</script>
<template>
<a v-if="isExternal" :href="to as string" target="_blank" rel="noopener" :class="$attrs.class"><slot></slot></a>
<nuxt-link v-else :to="to"><slot></slot></nuxt-link>
</template>

View File

@ -1,3 +1,46 @@
<script setup lang="ts">
const { data: stats } = useFetch('/api/admin/stats-public', { lazy: true });
const runtimeConfig = useRuntimeConfig();
const plausibleHost = runtimeConfig.public.plausibleHost;
const heartbeatHost = runtimeConfig.public.heartbeatHost;
const overall = ref(true);
const activeStats = computed(() => {
if (stats.value === null) {
return undefined;
}
return overall
? stats.value.overall
: stats.value.current || {};
});
const formatNumber = (number: number): string => {
if (number > 1000000) {
return `${Math.round(10 * number / 1000000) / 10}M`;
}
if (number > 1000) {
return `${Math.round(10 * number / 1000) / 10}k`;
}
return number.toString();
};
const formatDuration = (secondsCount: number): string => {
const minutes = Math.floor(secondsCount / 60);
const seconds = secondsCount % 60;
const res = [];
if (minutes > 0) {
res.push(`${minutes}m`);
}
if (seconds > 0) {
res.push(`${seconds}s`);
}
return res.join(' ');
};
</script>
<template>
<div v-if="stats !== null">
<p class="h6 mb-2">
@ -13,7 +56,7 @@
|
<strong><T>footer.stats.current</T></strong>
</p>
<ul :key="overall" class="list-unstyled">
<ul :key="`${overall}`" class="list-unstyled">
<li v-if="activeStats.cards" class="mb-2">
<Icon v="id-card" />
<T>footer.stats.keys.cards</T><T>quotation.colon</T>
@ -67,55 +110,3 @@
</ul>
</div>
</template>
<script>
export default {
setup() {
const { data: stats } = useFetch('/api/admin/stats-public', { lazy: true });
return {
stats,
};
},
data() {
return {
plausibleHost: this.$config.public.plausibleHost,
heartbeatHost: this.$config.public.heartbeatHost,
overall: true,
};
},
computed: {
activeStats() {
if (this.stats === null) {
return undefined;
}
return this.overall
? this.stats.overall
: this.stats.current || {};
},
},
methods: {
formatNumber(number) {
if (number > 1000000) {
return `${Math.round(10 * number / 1000000) / 10}M`;
}
if (number > 1000) {
return `${Math.round(10 * number / 1000) / 10}k`;
}
return number;
},
formatDuration(secondsCount) {
const minutes = Math.floor(secondsCount / 60);
const seconds = secondsCount % 60;
const res = [];
if (minutes > 0) {
res.push(`${minutes}m`);
}
if (seconds > 0) {
res.push(`${seconds}s`);
}
return res.join(' ');
},
},
};
</script>

View File

@ -1,3 +1,46 @@
<script setup lang="ts">
interface Link {
name: string;
route: string;
icon: string;
iconInverse?: boolean;
routesExtra?: string[];
condition?: boolean;
}
const props = withDefaults(defineProps<{
prefix?: string;
links: Link[];
extraClass?: string;
}>(), {
prefix: '',
extraClass: '',
});
const visibleLinks = computed((): Link[] => {
return props.links.filter((l) => l.condition === undefined || l.condition);
});
const buildRoute = (route: string): string => {
return `${props.prefix}/${route}`;
};
const route = useRoute();
const isActiveRoute = (path: string, routesExtra?: string[]): boolean => {
if (routesExtra && route.name && routesExtra.includes((route.name as string).split(':')[0])) {
return true;
}
let current = decodeURIComponent(route.fullPath).replace(/\/$/, '');
if (current.includes('#')) {
current = current.substring(0, current.indexOf('#'));
}
const expected = buildRoute(path).replace(/\/$/, '');
return current === expected || current.startsWith(`${expected}/`);
};
</script>
<template>
<section v-if="visibleLinks.length" class="mt-4 mt-lg-0 d-print-none">
<div class="d-none d-md-inline-flex btn-group btn-block mb-2 w-100">
@ -24,36 +67,3 @@
</div>
</section>
</template>
<script>
export default {
props: {
prefix: { default: '' },
links: { required: true },
extraClass: { default: '' },
},
computed: {
visibleLinks() {
return this.links.filter((l) => l.condition === undefined || l.condition === true);
},
},
methods: {
buildRoute(route) {
return `${this.prefix}/${route}`;
},
isActiveRoute(route, routesExtra) {
if (routesExtra && this.$route.name && routesExtra.includes(this.$route.name.split(':')[0])) {
return true;
}
let current = decodeURIComponent(this.$route.fullPath).replace(/\/$/, '');
if (current.includes('#')) {
current = current.substring(0, current.indexOf('#'));
}
const expected = this.buildRoute(route).replace(/\/$/, '');
return current === expected || current.startsWith(`${expected}/`);
},
},
};
</script>

View File

@ -1,3 +1,16 @@
<script setup lang="ts">
import forbidden from '../src/forbidden.ts';
definePageMeta({
translatedPaths: (config) => {
if (!config.user.enabled) {
return [];
}
return [`/${encodeURIComponent(config.user.termsRoute)}`];
},
});
</script>
<template>
<Page>
<h2>
@ -49,25 +62,3 @@
<p><T>terms.content.closing.changes</T></p>
</Page>
</template>
<script>
import forbidden from '../src/forbidden.js';
export default {
setup() {
definePageMeta({
translatedPaths: (config) => {
if (!config.user.enabled) {
return [];
}
return [`/${encodeURIComponent(config.user.termsRoute)}`];
},
});
},
data() {
return {
forbidden,
};
},
};
</script>

View File

@ -1,3 +1,53 @@
<script setup lang="ts">
const config = useConfig();
interface Link {
icon: string;
header: string;
route: string;
}
const mainLinks: Link[] = [];
if (config.pronouns.enabled) {
mainLinks.push({
icon: 'tags',
header: 'pronouns.headerLong',
route: config.pronouns.route,
});
}
if (config.nouns.enabled) {
mainLinks.push({
icon: 'book',
header: 'nouns.headerLong',
route: config.nouns.route,
});
}
if (config.user.enabled) {
mainLinks.push({
icon: 'id-card',
header: 'profile.bannerButton',
route: config.user.route,
});
}
const { data: latestPosts } = useAsyncData('latest-posts', async () => {
if (!config.blog) {
return [];
}
return (await $fetch('/api/blog')).slice(0, 9);
}, {
lazy: true,
});
const { data: featuredPosts } = useAsyncData('featured-posts', async () => {
if (!config.blog?.shortcuts) {
return [];
}
return await $fetch('/api/blog?shortcuts');
}, {
lazy: true,
});
</script>
<template>
<Page>
<AdPlaceholder :phkey="['header', null]" />
@ -12,8 +62,12 @@
</p>
<div class="row">
<div v-for="{ icon, header, route, link } in mainLinks" :class="[mainLinks.length > 3 ? 'col-6 col-lg-3' : 'col', 'my-4', 'text-center']">
<LocaleLink :link="link || route" :locale="link ? 'external' : config.locale">
<div
v-for="{ icon, header, route } in mainLinks"
:key="header"
:class="[mainLinks.length > 3 ? 'col-6 col-lg-3' : 'col', 'my-4', 'text-center']"
>
<LocaleLink :link="route" :locale="config.locale">
<p>
<Icon :v="icon" :size="2" class="d-inline-block d-lg-none" />
<Icon :v="icon" :size="3" class="d-none d-lg-inline-block" />
@ -115,78 +169,3 @@
</section>
</Page>
</template>
<script>
import { mapState } from 'pinia';
import useConfig from '../composables/useConfig.ts';
import { useMainStore } from '../store/index.ts';
export default {
setup() {
const config = useConfig();
const { data: latestPosts } = useAsyncData('latest-posts', async () => {
if (!config.blog) {
return [];
}
return (await $fetch('/api/blog')).slice(0, 9);
}, {
lazy: true,
});
const { data: featuredPosts } = useAsyncData('featured-posts', async () => {
if (!config.blog?.shortcuts) {
return [];
}
return await $fetch('/api/blog?shortcuts');
}, {
lazy: true,
});
return {
config,
latestPosts,
featuredPosts,
};
},
computed: {
...mapState(useMainStore, [
'user',
]),
},
data() {
const mainLinks = [];
if (this.config.pronouns.enabled) {
mainLinks.push({
icon: 'tags',
header: 'pronouns.headerLong',
route: this.config.pronouns.route,
});
}
if (this.config.nouns.enabled) {
mainLinks.push({
icon: 'book',
header: 'nouns.headerLong',
route: this.config.nouns.route,
});
}
if (this.config.user.enabled) {
mainLinks.push({
icon: 'id-card',
header: 'profile.bannerButton',
route: this.config.user.route,
});
}
// if (this.config.locale === 'pl') {
// mainLinks.push({
// icon: 'isjp.svg',
// header: 'isjp.homepage',
// link: 'https://isjp.pl',
// })
// }
return {
mainLinks,
};
},
};
</script>

View File

@ -7,6 +7,7 @@ export interface User {
avatarSource: string;
bannedReason: string;
usernameNorm: string;
bannedTerms?: string;
bannedBy: string;
lastActive: number;
banSnapshot: string;