Files
pagerite/frontend/src/TrailLink.vue
T
LeoVasanko 2843469e04 contact messages: client proof flow, upload re-auth, trail ✉️
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.
2026-09-28 14:39:49 +00:00

83 lines
2.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>