PronounsPage/layouts/default.vue
2024-07-04 21:43:44 +02:00

268 lines
9.3 KiB
Vue

<template>
<div v-if="requiresLogin && !testerPasswordValid" class="body">
<div class="container">
<div class="alert alert-warning m-3 text-center">
<Icon v="exclamation-triangle" />
This is a test server
</div>
<div class="m-3">
<div class="input-group py-1">
<input v-model="testerPassword" class="form-control" type="password" placeholder="Password" @keydown.enter.prevent="checkTesterPassword">
<button type="button" class="btn btn-primary btn-sm border" @click.prevent="checkTesterPassword">
<Icon v="sign-in" />
Sign in
</button>
</div>
<p v-if="testerPasswordCookie && !testerPasswordValid" class="small text-danger">
<Icon v="exclamation-triangle" />
Password invalid
</p>
</div>
</div>
</div>
<div v-else class="body">
<div class="flex-grow-1 vh">
<Header />
<Nuxt />
<TranslationMode />
<ScrollButton />
</div>
<Footer />
<DialogueBox ref="dialogue" />
<Lightbox />
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import type { AxiosRequestConfig } from 'axios';
import dark from '../plugins/dark.ts';
import sorter from 'avris-sorter';
import { sleep } from '../src/helpers.ts';
import md5 from 'js-md5';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import { LoadScriptError } from '../src/errors.ts';
import type { default as dialogueBox, DialogueMessage } from '../components/DialogueBox.vue';
import type { Color } from '../src/bootstrap.ts';
type DialogueBox = InstanceType<typeof dialogueBox>;
// no need to be super secure, just a sign that the page is not public
const TESTER_PASSWORD_COOKIE_KEY = 'tester-password';
const TESTER_PASSWORD_HASH = '82feeb96d60170e714df8fb062301e90';
declare global {
interface Window {
dataLayer: unknown[];
}
}
declare module 'vue/types/vue' {
interface Vue {
$alert(message: string | DialogueMessage, color?: Color): Promise<string | string[] | undefined>;
$confirm(message?: string | DialogueMessage, color?: Color): Promise<string | string[] | undefined>;
$editor(
value: string | string[],
message?: string | DialogueMessage,
color?: Color,
): Promise<string | string[] | undefined>;
$alertRaw(message: string | DialogueMessage, color?: Color): Promise<string | string[] | undefined>;
$post<T = unknown>(url: string, data?: unknown, options?: AxiosRequestConfig, timeout?: number): Promise<T>;
}
}
export default dark.extend({
data(): { testerPassword: string, testerPasswordCookie: string } {
return {
testerPassword: '',
testerPasswordCookie: this.$cookies.get(TESTER_PASSWORD_COOKIE_KEY),
};
},
computed: {
adsEnabled(): boolean {
if (this.$isGranted()) {
const adsVisible = parseInt(localStorage.getItem('adsVisible') || '0') === 1;
if (!adsVisible) {
return false;
}
}
return !!this.$config.ads?.enabled && process.env.NODE_ENV !== 'development';
},
requiresLogin(): boolean {
return process.env.NODE_ENV === 'test' ||
this.$config.locale !== '_' && !this.$locales[this.$config.locale]?.published;
},
testerPasswordValid(): boolean {
return !!this.testerPasswordCookie && md5(this.testerPasswordCookie) === TESTER_PASSWORD_HASH;
},
dialogue(): DialogueBox {
return this.$refs.dialogue as DialogueBox;
},
},
mounted() {
Vue.prototype.$alert = (
message: string | DialogueMessage,
color: Color = 'primary',
): Promise<string | string[] | undefined> => {
return new Promise((resolve, reject) => {
this.dialogue.show(false, message, color, undefined, undefined, resolve, reject);
});
};
Vue.prototype.$confirm = (
message: string | DialogueMessage = '',
color: Color = 'primary',
): Promise<string | string[] | undefined> => {
return new Promise((resolve, reject) => {
this.dialogue.show(true, message, color, undefined, undefined, resolve, reject);
});
};
Vue.prototype.$editor = (
value: string | string[],
message: string | DialogueMessage = '',
color: Color = 'primary',
): Promise<string | string[] | undefined> => {
return new Promise((resolve, reject) => {
this.dialogue.show(false, message, color, value, 'lg', resolve, reject);
});
};
Vue.prototype.$alertRaw = (
message: string | DialogueMessage,
color: Color = 'primary',
): Promise<string | string[] | undefined> => {
return new Promise((resolve, reject) => {
this.dialogue.show(
false,
`<pre class="text-start"><code>${message}</code></pre>`,
color,
undefined,
'lg',
resolve,
reject,
);
});
};
Vue.prototype.$post = <T = unknown>(
url: string,
data: unknown = undefined,
options: AxiosRequestConfig = {},
timeout = 30000,
): Promise<T> => {
return new Promise((resolve, reject) => {
this.$axios.$post(url, data, { ...options, timeout })
.then((data) => resolve(data))
.catch(async (error) => {
let errorMessage = this.$t('error.generic');
if (typeof error.response?.data?.error === 'string') {
errorMessage = this.$t(error.response?.data?.error);
// in case no translatable key was provided
if (errorMessage === undefined) {
errorMessage = error.response?.data?.error;
}
}
await this.$alert(errorMessage, 'danger');
reject(new Error(`POST to ${url} failed: ${error.response?.data?.error || 'unknown error'}`));
});
});
};
sorter();
this.confirmAge();
this.loadAds();
this.loadGTM();
let needsRefresh = false;
const bc = new BroadcastChannel('account_switch');
bc.onmessage = (ev): void => {
if (ev.data !== this.$user()?.username) {
needsRefresh = true;
if (document.hasFocus()) {
needsRefresh = false;
window.location.reload();
}
}
};
window.onfocus = (): void => {
if (needsRefresh) {
needsRefresh = false;
window.location.reload();
}
};
},
methods: {
async confirmAge(): Promise<void> {
if (!this.$te('footer.ageLimit') || localStorage.getItem('ageConfirmed')) {
return;
}
while (this.$refs.dialogue === undefined) {
await sleep(100);
}
await this.$alert(this.$t('footer.ageLimit'));
localStorage.setItem('ageConfirmed', '1');
},
async loadAds(): Promise<void> {
if (!this.adsEnabled) {
return;
}
try {
await this.$loadScript('publift', 'https://cdn.fuseplatform.net/publift/tags/2/3329/fuse.js');
} catch (error) {
if (error instanceof LoadScriptError) {
return;
}
throw error;
}
},
async loadGTM(): Promise<void> {
if (!this.adsEnabled) {
return;
}
try {
await this.$loadScript('gtm', 'https://www.googletagmanager.com/gtag/js?id=G-TDJEP12Q3M');
} catch (error) {
if (error instanceof LoadScriptError) {
return;
}
throw error;
}
window.dataLayer = window.dataLayer || [];
function gtag(..._args: unknown[]): void {
window.dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-TDJEP12Q3M');
},
checkTesterPassword(): void {
this.$cookies.set(TESTER_PASSWORD_COOKIE_KEY, this.testerPassword, longtimeCookieSetting);
this.testerPasswordCookie = this.testerPassword;
},
},
});
</script>
<style lang="scss">
@import "assets/variables";
@import "~avris-sorter/dist/Sorter.min.css";
@include media-breakpoint-up('lg', $grid-breakpoints) {
.body {
margin-top: $header-margin;
}
.sticky-top {
top: $header-height - 1px;
}
}
.vh {
min-height: calc(100vh - #{$header-height});
}
</style>