The contact bundle submits each solved challenge immediately and treats
{quota} frames as the grant that releases queued sends/uploads; an
upload answered 429 "pow" re-registers for a fresh challenge and
retries once it lands (Composer's reauthorize prop). The contact email
frame fills the dialog's mailto link only after a solve. Visit trails
render the ✉️ marker on the step where the panel was opened and the
proof solved.
83 lines
2.1 KiB
Vue
83 lines
2.1 KiB
Vue
<script setup>
|
||
import { computed } from 'vue'
|
||
import { formatCount, formatReadTime } from './analytics/format.js'
|
||
|
||
const props = defineProps({
|
||
step: { type: Object, required: true },
|
||
count: { type: Number, default: 0 },
|
||
favicons: { type: Object, default: null },
|
||
flags: { type: Array, default: () => [] },
|
||
})
|
||
|
||
defineEmits(['close'])
|
||
|
||
const hasError = computed(() => props.step.status >= 400)
|
||
const favicon = computed(() =>
|
||
props.step.external && props.step.origin ? props.favicons?.[props.step.origin] : null,
|
||
)
|
||
|
||
const title = computed(() => {
|
||
const parts = [props.step.title]
|
||
if (props.step.readSeconds > 0) {
|
||
parts.push(formatReadTime(props.step.readSeconds))
|
||
}
|
||
if (hasError.value) {
|
||
parts.push(`${props.step.status}`)
|
||
}
|
||
if (props.step.contact) {
|
||
parts.push('contact panel opened, proof of work solved')
|
||
}
|
||
return parts.filter(Boolean).join(' — ')
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<a class="trail-link"
|
||
:class="{ error: hasError }"
|
||
:href="step.path"
|
||
:title="title"
|
||
:target="step.external ? '_blank' : undefined"
|
||
:rel="step.external ? 'noopener' : undefined"
|
||
@click="(e) => { if (!step.external) $emit('close') }">
|
||
<small v-if="count > 1" class="muted">{{ formatCount(count) }}×</small>
|
||
<img v-if="favicon" class="favicon" :src="favicon" alt="" />
|
||
<span>{{ step.slug }}</span>
|
||
<span v-if="step.contact" class="contact-mark">✉️</span>
|
||
<span v-for="(f, fi) in flags" :key="fi" class="flag" v-html="f"></span>
|
||
</a>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.favicon {
|
||
width: 1em;
|
||
height: 1em;
|
||
margin-right: 0.25em;
|
||
vertical-align: -0.1em;
|
||
}
|
||
|
||
/* The contact-panel marker (PoW solved on this step). */
|
||
.contact-mark {
|
||
margin-left: 0.25em;
|
||
font-size: 0.85em;
|
||
}
|
||
|
||
/* Same flag chip as the visitor cells (VisitorCell.vue). */
|
||
.flag {
|
||
display: inline-flex;
|
||
width: 18px;
|
||
height: 12px;
|
||
margin-left: 0.25em;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--line);
|
||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2) inset;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.flag :deep(svg) {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
</style>
|