2022-07-12 10:55:28 +02:00
< 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"
/ >
< img
2022-08-12 16:40:04 +02:00
: src = "`/img/pics/error-480w.webp`"
2022-07-12 10:55:28 +02:00
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 - n o t i f i c a t i o n >
< / 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."
>
2022-08-12 16:42:40 +02:00
< template # instanceName >
2022-07-12 10:55:28 +02:00
< b > { { config ? . name } } < / b >
< / template >
2022-08-12 16:42:40 +02:00
< template # mobilizon_link >
2022-07-12 10:55:28 +02:00
< a href = "https://joinmobilizon.org" > { { t ( "Mobilizon" ) } } < / a >
< / template >
< / i 1 8 n - t >
< span v-if ="sentryEnabled" >
{ {
t (
"We collect your feedback and the error information in order to improve this service."
)
} } < / s p a n
>
< 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 >
2022-08-26 16:08:58 +02:00
< summary > { { t ( "Technical details" ) } } < / summary >
2022-07-12 10:55:28 +02:00
< 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 - b u t t o n
>
< / o - t o o l t i p >
< / 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 : 2 rem ;
}
. picture - wrapper {
text - align : center ;
}
details {
summary : hover {
cursor : pointer ;
}
}
}
< / style >