contact messages: settings UI, localized dialog title, mailto link
The site editor gains a messaging block: enabled toggle, contact email, and label/subject fields with a language selector that switches them to editing per-language overrides (the manual-override UI for the strings auto translators fill). The visitor dialog titles itself with the localized label and shows a mailto link (localized subject prefilled) when an email is configured; pagerite.js hides the corner icon from visitors when messaging is disabled.
This commit is contained in:
1 parent
f5fbe8ddc5
commit
dd82005f6c
4 files changed
+201
-5
No files matched your search
@@ -17,6 +17,13 @@ const emit = defineEmits(['send', 'close'])
|
||||
|
||||
const admin = computed(() => props.state.mode === 'admin')
|
||||
|
||||
// The visitor dialog's title is the localized site contact label (the
|
||||
// {contact:} buttons' default label); the mailto link carries the
|
||||
// localized subject.
|
||||
const mailto = computed(
|
||||
() => `mailto:${props.state.email}?subject=${encodeURIComponent(props.state.subject)}`
|
||||
)
|
||||
|
||||
// Escape closes, like the paskia dialogs whose backdrop we share.
|
||||
function onKey(ev) {
|
||||
if (ev.key === 'Escape' && props.state.open) emit('close')
|
||||
@@ -31,10 +38,11 @@ onUnmounted(() => removeEventListener('keydown', onKey))
|
||||
class="contact-panel"
|
||||
:class="{ admin }"
|
||||
role="dialog"
|
||||
:aria-label="admin ? 'Messages' : 'Contact us'"
|
||||
:aria-label="admin ? 'Messages' : state.label"
|
||||
>
|
||||
<header>
|
||||
<h2>{{ admin ? 'Messages' : 'Contact us' }}</h2>
|
||||
<h2>{{ admin ? 'Messages' : state.label }}</h2>
|
||||
<a v-if="!admin && state.email" class="mailto" :href="mailto">{{ state.email }}</a>
|
||||
<button type="button" class="close" title="close" @click="emit('close')">✕</button>
|
||||
</header>
|
||||
<ContactPanel v-if="!admin" :state="state" :token="token" @send="(p) => emit('send', p)" />
|
||||
@@ -80,11 +88,24 @@ onUnmounted(() => removeEventListener('keydown', onKey))
|
||||
.contact-panel header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.contact-panel h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.contact-panel .mailto {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.contact-panel .mailto:hover {
|
||||
color: var(--text, CanvasText);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.contact-panel .close {
|
||||
margin-left: auto;
|
||||
padding: 0 0.3rem;
|
||||
|
||||
@@ -57,6 +57,69 @@ async function errorDetail(res) {
|
||||
// empty brand removes the header link and the title suffix entirely.
|
||||
const brand = ref('')
|
||||
const theme = ref('')
|
||||
|
||||
// --- Messaging (contact) ---------------------------------------------------
|
||||
// Site-wide contact settings: the enabled toggle and mailto address are
|
||||
// global; the label (the {contact:} buttons' default and the visitor
|
||||
// dialog's title) and the email subject are per-language strings — the
|
||||
// selector switches the two text fields between the original and a
|
||||
// translation override (stored in the chunk-keyed trans store; auto
|
||||
// translators fill the same store, so this is the manual-override UI).
|
||||
const contactEnabled = ref(true)
|
||||
const contactEmail = ref('')
|
||||
const contactLabel = ref('')
|
||||
const contactSubject = ref('')
|
||||
const contactLabelOrig = ref('')
|
||||
const contactSubjectOrig = ref('')
|
||||
const contactLang = ref('') // '' = the original strings
|
||||
const primaryLang = ref('en')
|
||||
const translateLangs = ref([])
|
||||
const translating = computed(() => contactLang.value && contactLang.value !== primaryLang.value)
|
||||
|
||||
async function onContactLangChange() {
|
||||
if (!translating.value) {
|
||||
contactLabel.value = contactLabelOrig.value
|
||||
contactSubject.value = contactSubjectOrig.value
|
||||
return
|
||||
}
|
||||
try {
|
||||
const s = await apiJson(`/_api/settings?lang=${contactLang.value}`)
|
||||
contactLabel.value = s.contact_label_tr || ''
|
||||
contactSubject.value = s.contact_subject_tr || ''
|
||||
} catch { /* keep empty */ }
|
||||
}
|
||||
|
||||
function onContactInput() {
|
||||
// Capture the language being edited: a debounced save must not land in
|
||||
// whatever language happens to be selected when the timer fires.
|
||||
const lang = contactLang.value
|
||||
if (!lang || lang === primaryLang.value) {
|
||||
contactLabelOrig.value = contactLabel.value
|
||||
contactSubjectOrig.value = contactSubject.value
|
||||
}
|
||||
debounce('contact', () => saveContact(lang, contactLabel.value, contactSubject.value))
|
||||
}
|
||||
|
||||
async function saveContact(lang = contactLang.value, label = '', subject = '') {
|
||||
if (lang && lang !== primaryLang.value) {
|
||||
// Translation override: only the label/subject apply (empty = drop).
|
||||
const res = await apiFetch(`/_api/settings?lang=${lang}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
brand: brand.value,
|
||||
theme: theme.value,
|
||||
custom_css: customCss.value,
|
||||
contact_label: label,
|
||||
contact_subject: subject,
|
||||
}),
|
||||
})
|
||||
saveError.value = res.ok ? '' : '⚠️ changes could not be saved'
|
||||
if (res.ok) dropPageCache()
|
||||
return
|
||||
}
|
||||
saveSettings()
|
||||
}
|
||||
// Theme options come from the backend (theme folders on disk, see GET
|
||||
// /_api/settings), so added themes need no frontend changes.
|
||||
const themeOptions = ref([{ value: '', label: '☀️ none' }])
|
||||
@@ -88,6 +151,16 @@ async function loadSettings() {
|
||||
]
|
||||
transition.value = s.transition || 'cube'
|
||||
transitionOptions.value = s.transitions || []
|
||||
contactEnabled.value = s.contact_enabled !== false
|
||||
contactEmail.value = s.contact_email || ''
|
||||
contactLabelOrig.value = s.contact_label || ''
|
||||
contactSubjectOrig.value = s.contact_subject || ''
|
||||
if (!translating.value) {
|
||||
contactLabel.value = contactLabelOrig.value
|
||||
contactSubject.value = contactSubjectOrig.value
|
||||
}
|
||||
primaryLang.value = s.primary_lang || 'en'
|
||||
translateLangs.value = s.translate_langs || []
|
||||
fontOptions.value = [
|
||||
...BASE_FONT_OPTIONS,
|
||||
...(s.fonts || []).map((f) => ({
|
||||
@@ -255,6 +328,12 @@ async function saveSettings(opts = {}) {
|
||||
custom_css: customCss.value,
|
||||
brand_html: brandHtml.value,
|
||||
transition: transition.value,
|
||||
contact_enabled: contactEnabled.value,
|
||||
contact_email: contactEmail.value,
|
||||
// The originals: while a translation override is being edited the
|
||||
// input refs hold the override, not the original.
|
||||
contact_label: contactLabelOrig.value,
|
||||
contact_subject: contactSubjectOrig.value,
|
||||
...opts,
|
||||
}),
|
||||
})
|
||||
@@ -655,6 +734,63 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<div class="block-head">
|
||||
<span class="field-label">messaging</span>
|
||||
<select
|
||||
v-if="translateLangs.length"
|
||||
v-model="contactLang"
|
||||
class="text-input theme-select contact-lang"
|
||||
title="Language of the label/subject strings (auto translators fill these too; this edits the manual override)"
|
||||
@change="onContactLangChange"
|
||||
>
|
||||
<option value="">original</option>
|
||||
<option v-for="l in translateLangs" :key="l" :value="l">{{ l }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
<label class="field">
|
||||
<span class="field-label">enabled</span>
|
||||
<input
|
||||
v-model="contactEnabled"
|
||||
type="checkbox"
|
||||
title="Visitor messaging ({contact:} buttons, corner icon)"
|
||||
@change="saveSettings()"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">email</span>
|
||||
<input
|
||||
v-model="contactEmail"
|
||||
class="text-input"
|
||||
title="Contact email — a mailto link in the visitor dialog (empty = no link)"
|
||||
:disabled="!!translating"
|
||||
@input="debounce('contact-email', () => saveSettings())"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">label</span>
|
||||
<input
|
||||
v-model="contactLabel"
|
||||
class="text-input"
|
||||
title="Default {contact:} button label and visitor dialog title (empty = built-in default)"
|
||||
:placeholder="translating ? contactLabelOrig : 'Contact us'"
|
||||
@input="onContactInput"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label">subject</span>
|
||||
<input
|
||||
v-model="contactSubject"
|
||||
class="text-input"
|
||||
title="Subject for the mailto link (empty = the label)"
|
||||
:placeholder="translating ? contactSubjectOrig : ''"
|
||||
@input="onContactInput"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block grow" @paste="onBrandPaste">
|
||||
<div class="block-head">
|
||||
<span class="field-label">brand code (replaces the brand link)</span>
|
||||
@@ -785,6 +921,13 @@ onUnmounted(() => {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* The messaging block's language selector sits at the block head's right. */
|
||||
.block-head .contact-lang {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
flex: 1;
|
||||
min-width: 4rem;
|
||||
|
||||
@@ -25,8 +25,34 @@ const state = reactive({
|
||||
unread: false, // visitor: an unread admin reply
|
||||
adminUnread: 0, // admin: unanswered threads
|
||||
viewerAdmin: false, // set once the inbox has been opened: icon badge = adminUnread
|
||||
// Localized site contact strings (pagerite:contact-* asset config,
|
||||
// refreshed on every open — they change with the page language):
|
||||
// label doubles as the visitor dialog's title; email gets a mailto
|
||||
// link with subject prefilled.
|
||||
label: 'Contact us',
|
||||
email: '',
|
||||
subject: '',
|
||||
})
|
||||
|
||||
// The localized contact strings ride the page's asset config (the
|
||||
// production JSON block, or pagerite:* meta tags in dev).
|
||||
function readContactStrings() {
|
||||
let assets = {}
|
||||
const el = document.getElementById('pagerite-assets')
|
||||
if (el) {
|
||||
try {
|
||||
assets = JSON.parse(el.textContent)
|
||||
} catch { /* keep empty */ }
|
||||
} else {
|
||||
for (const m of document.querySelectorAll('meta[name^="pagerite:contact-"]')) {
|
||||
assets[m.name] = m.content
|
||||
}
|
||||
}
|
||||
state.label = assets['pagerite:contact-label'] || 'Contact us'
|
||||
state.subject = assets['pagerite:contact-subject'] || state.label
|
||||
state.email = assets['pagerite:contact-email'] || ''
|
||||
}
|
||||
|
||||
// The panel is main content, not a sidebar: while open it holds paskia's
|
||||
// shared blur backdrop (refcounted, z-index 1099; the overlay itself sits
|
||||
// at 1100, below the auth iframe's 9999).
|
||||
@@ -154,6 +180,7 @@ export function open(br, { admin = false } = {}) {
|
||||
bridge.register(token, onThread, onPow)
|
||||
}
|
||||
state.mode = admin ? 'admin' : 'visitor'
|
||||
readContactStrings()
|
||||
state.open = true
|
||||
if (!admin) markSeen()
|
||||
else refreshBadges()
|
||||
|
||||
@@ -191,12 +191,16 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
// contact_unread. Once the bundle has loaded, its refreshBadges keeps
|
||||
// the marker live.
|
||||
let adminUnreadSeed = 0;
|
||||
// Messaging disabled in the site settings (no pagerite:contact-label in
|
||||
// the asset config): no icon for visitors — admins keep theirs, the
|
||||
// inbox still holds the existing threads.
|
||||
const contactEnabled = !!assets["pagerite:contact-label"];
|
||||
function makeContactIcon(admin) {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.id = "contact-icon";
|
||||
b.className = "contact-icon icon-btn";
|
||||
b.title = admin ? "messages" : "contact us";
|
||||
b.title = admin ? "messages" : (assets["pagerite:contact-label"] || "contact us");
|
||||
b.textContent = "✉️";
|
||||
if (admin) {
|
||||
if (adminUnreadSeed > 0) b.classList.add("has-unread");
|
||||
@@ -245,8 +249,9 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
const onToolPage = currentPath === "/_a";
|
||||
if (document.getElementById("page-banner")) {
|
||||
const pens = pensContainer();
|
||||
// The contact icon is there for everyone.
|
||||
pens.append(makeContactIcon(canEdit));
|
||||
// The contact icon is there for everyone — except visitors when
|
||||
// messaging is disabled (admins keep the inbox).
|
||||
if (canEdit || contactEnabled) pens.append(makeContactIcon(canEdit));
|
||||
if (canEdit && !onToolPage) {
|
||||
// Analytics viewer is a normal page at /_a.
|
||||
const a = document.createElement("a");
|
||||
|
||||
Reference in new issue
Block a user