forked from potsda.mn/mobilizon
217 lines
5.9 KiB
Vue
217 lines
5.9 KiB
Vue
|
<template>
|
||
|
<div class="container mx-auto" id="error-wrapper">
|
||
|
<div class="">
|
||
|
<section>
|
||
|
<div class="text-center">
|
||
|
<picture>
|
||
|
<source
|
||
|
:srcset="`/img/pics/error-480w.webp 1x, /img/pics/error-1024w.webp 2x`"
|
||
|
type="image/webp"
|
||
|
/>
|
||
|
<source
|
||
|
:srcset="`/img/pics/error-480w.jpg 1x, /img/pics/error-1024w.jpg 2x`"
|
||
|
type="image/jpeg"
|
||
|
/>
|
||
|
|
||
|
<img
|
||
|
:src="`/img/pics/error-480w.jpg`"
|
||
|
alt=""
|
||
|
width="480"
|
||
|
height="312"
|
||
|
loading="lazy"
|
||
|
/>
|
||
|
</picture>
|
||
|
</div>
|
||
|
<o-notification variant="danger" class="">
|
||
|
<h1>
|
||
|
{{
|
||
|
t(
|
||
|
"An error has occured. Sorry about that. You may try to reload the page."
|
||
|
)
|
||
|
}}
|
||
|
</h1>
|
||
|
</o-notification>
|
||
|
</section>
|
||
|
<o-loading v-if="loading" v-model:active="loading" />
|
||
|
<section v-else>
|
||
|
<h2 class="">{{ t("What can I do to help?") }}</h2>
|
||
|
<p class="prose dark:prose-invert">
|
||
|
<i18n-t
|
||
|
tag="span"
|
||
|
keypath="{instanceName} is an instance of {mobilizon_link}, a free software built with the community."
|
||
|
>
|
||
|
<template v-slot:instanceName>
|
||
|
<b>{{ config?.name }}</b>
|
||
|
</template>
|
||
|
<template v-slot:mobilizon_link>
|
||
|
<a href="https://joinmobilizon.org">{{ t("Mobilizon") }}</a>
|
||
|
</template>
|
||
|
</i18n-t>
|
||
|
<span v-if="sentryEnabled">
|
||
|
{{
|
||
|
t(
|
||
|
"We collect your feedback and the error information in order to improve this service."
|
||
|
)
|
||
|
}}</span
|
||
|
>
|
||
|
<span v-else>
|
||
|
{{
|
||
|
t(
|
||
|
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
|
||
|
)
|
||
|
}}
|
||
|
</span>
|
||
|
</p>
|
||
|
<SentryFeedback />
|
||
|
|
||
|
<p class="prose dark:prose-invert" v-if="!sentryEnabled">
|
||
|
{{
|
||
|
t(
|
||
|
"Please add as many details as possible to help identify the problem."
|
||
|
)
|
||
|
}}
|
||
|
</p>
|
||
|
|
||
|
<details>
|
||
|
<summary class="is-size-5">{{ t("Technical details") }}</summary>
|
||
|
<p>{{ t("Error message") }}</p>
|
||
|
<pre>{{ error }}</pre>
|
||
|
<p>{{ t("Error stacktrace") }}</p>
|
||
|
<pre>{{ error.stack }}</pre>
|
||
|
</details>
|
||
|
<p v-if="!sentryEnabled">
|
||
|
{{
|
||
|
t(
|
||
|
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
|
||
|
)
|
||
|
}}
|
||
|
</p>
|
||
|
<div class="buttons" v-if="!sentryEnabled">
|
||
|
<o-tooltip
|
||
|
:label="tooltipConfig.label"
|
||
|
:type="tooltipConfig.type"
|
||
|
:active="copied !== false"
|
||
|
always
|
||
|
>
|
||
|
<o-button
|
||
|
@click="copyErrorToClipboard"
|
||
|
@keyup.enter="copyErrorToClipboard"
|
||
|
>{{ t("Copy details to clipboard") }}</o-button
|
||
|
>
|
||
|
</o-tooltip>
|
||
|
</div>
|
||
|
</section>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
<script lang="ts" setup>
|
||
|
import { checkProviderConfig } from "@/services/statistics";
|
||
|
import { IAnalyticsConfig } from "@/types/config.model";
|
||
|
import { computed, defineAsyncComponent, ref } from "vue";
|
||
|
import { useQueryLoading } from "@vue/apollo-composable";
|
||
|
import { useI18n } from "vue-i18n";
|
||
|
import { useHead } from "@vueuse/head";
|
||
|
import { useAnalytics } from "@/composition/apollo/config";
|
||
|
const SentryFeedback = defineAsyncComponent(
|
||
|
() => import("./Feedback/SentryFeedback.vue")
|
||
|
);
|
||
|
|
||
|
const { analytics } = useAnalytics();
|
||
|
|
||
|
const loading = useQueryLoading();
|
||
|
|
||
|
const props = defineProps<{
|
||
|
error: Error;
|
||
|
}>();
|
||
|
|
||
|
const copied = ref<"success" | "error" | false>(false);
|
||
|
|
||
|
const { t } = useI18n({ useScope: "global" });
|
||
|
useHead({
|
||
|
title: computed(() => t("Error")),
|
||
|
});
|
||
|
|
||
|
const copyErrorToClipboard = async (): Promise<void> => {
|
||
|
try {
|
||
|
if (window.isSecureContext && navigator.clipboard) {
|
||
|
await navigator.clipboard.writeText(fullErrorString.value);
|
||
|
} else {
|
||
|
fallbackCopyTextToClipboard(fullErrorString.value);
|
||
|
}
|
||
|
copied.value = "success";
|
||
|
setTimeout(() => {
|
||
|
copied.value = false;
|
||
|
}, 2000);
|
||
|
} catch (e) {
|
||
|
copied.value = "error";
|
||
|
console.error("Unable to copy to clipboard");
|
||
|
console.error(e);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const fullErrorString = computed((): string => {
|
||
|
return `${props.error.name}: ${props.error.message}\n\n${props.error.stack}`;
|
||
|
});
|
||
|
|
||
|
const tooltipConfig = computed(
|
||
|
(): { label: string | null; variant: string | null } => {
|
||
|
if (copied.value === "success")
|
||
|
return {
|
||
|
label: t("Error details copied!") as string,
|
||
|
variant: "success",
|
||
|
};
|
||
|
if (copied.value === "error")
|
||
|
return {
|
||
|
label: t("Unable to copy to clipboard") as string,
|
||
|
variant: "danger",
|
||
|
};
|
||
|
return { label: null, variant: "primary" };
|
||
|
}
|
||
|
);
|
||
|
|
||
|
const fallbackCopyTextToClipboard = (text: string): void => {
|
||
|
const textArea = document.createElement("textarea");
|
||
|
textArea.value = text;
|
||
|
|
||
|
// Avoid scrolling to bottom
|
||
|
textArea.style.top = "0";
|
||
|
textArea.style.left = "0";
|
||
|
textArea.style.position = "fixed";
|
||
|
|
||
|
document.body.appendChild(textArea);
|
||
|
textArea.focus();
|
||
|
textArea.select();
|
||
|
|
||
|
document.execCommand("copy");
|
||
|
|
||
|
document.body.removeChild(textArea);
|
||
|
};
|
||
|
|
||
|
const sentryEnabled = computed((): boolean => {
|
||
|
return sentryProvider.value?.enabled === true;
|
||
|
});
|
||
|
|
||
|
const sentryProvider = computed((): IAnalyticsConfig | undefined => {
|
||
|
return checkProviderConfig(analytics.value ?? [], "sentry");
|
||
|
});
|
||
|
</script>
|
||
|
<style lang="scss" scoped>
|
||
|
#error-wrapper {
|
||
|
width: 100%;
|
||
|
|
||
|
section {
|
||
|
margin-bottom: 2rem;
|
||
|
}
|
||
|
|
||
|
.picture-wrapper {
|
||
|
text-align: center;
|
||
|
}
|
||
|
|
||
|
details {
|
||
|
summary:hover {
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
</style>
|