Add local and public QR sharing for training profiles
This commit is contained in:
@@ -114,7 +114,14 @@ export function renderTrainingView(deps) {
|
||||
&& (!goal.classId || String(goal.classId) === selectedClassId)
|
||||
));
|
||||
const profileShare = (state.training.shares || []).find((share) => profileReferences.has(String(share.driverId || "")) && share.enabled);
|
||||
const publicProfileUrl = profileShare ? buildPublicProfileUrl(profileShare.token, state.settings?.language) : "";
|
||||
const publicBaseUrl = normalizePublicBaseUrl(state.settings?.publicBaseUrl);
|
||||
const localBaseUrl = window.location.origin;
|
||||
const publicProfileUrl = profileShare ? buildPublicProfileUrl(profileShare.token, state.settings?.language, publicBaseUrl || localBaseUrl) : "";
|
||||
const qrLanguage = state.settings?.language === "en" ? "en" : "sv";
|
||||
const localQrUrl = profileShare ? buildPublicProfileQrUrl(profileShare.token, qrLanguage, "local") : "";
|
||||
const publicQrUrl = profileShare && publicBaseUrl && publicBaseUrl !== localBaseUrl
|
||||
? buildPublicProfileQrUrl(profileShare.token, qrLanguage, publicBaseUrl)
|
||||
: "";
|
||||
const driverOptions = `<option value="">${escapeHtml(t("training.overview_title"))}</option>` + profiles
|
||||
.map((profile) => {
|
||||
const classNames = profile.classIds
|
||||
@@ -439,11 +446,36 @@ export function renderTrainingView(deps) {
|
||||
</div>
|
||||
<div class="training-share-compact">
|
||||
<p>${escapeHtml(t("training.public_profile_help"))}</p>
|
||||
<label class="training-share-url">
|
||||
<span>${escapeHtml(t("training.public_base_url"))}</span>
|
||||
<input id="trainingPublicBaseUrl" type="url" value="${escapeHtml(publicBaseUrl)}" placeholder="https://training.example.se" />
|
||||
<small>${escapeHtml(t("training.public_base_url_hint"))}</small>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button class="btn" id="trainingPublicBaseSave" type="button">${escapeHtml(t("training.save_public_url"))}</button>
|
||||
<button class="btn" id="trainingPublicAddressDetect" type="button">${escapeHtml(t("training.detect_public_ip"))}</button>
|
||||
</div>
|
||||
<p class="hint" id="trainingPublicAddressStatus"></p>
|
||||
${profileShare ? `
|
||||
<label class="training-share-url">
|
||||
<span>${escapeHtml(t("training.public_profile_link"))}</span>
|
||||
<input id="trainingPublicProfileUrl" value="${escapeHtml(publicProfileUrl)}" readonly />
|
||||
</label>
|
||||
<div class="training-profile-qr-list">
|
||||
<div class="training-profile-qr">
|
||||
<strong>${escapeHtml(t("training.local_profile_qr"))}</strong>
|
||||
<img src="${escapeHtml(localQrUrl)}" alt="${escapeHtml(t("training.local_profile_qr"))}" />
|
||||
<small>${escapeHtml(t("training.local_profile_qr_hint"))}</small>
|
||||
</div>
|
||||
${publicQrUrl ? `
|
||||
<div class="training-profile-qr">
|
||||
<strong>${escapeHtml(t("training.public_profile_qr"))}</strong>
|
||||
<img src="${escapeHtml(publicQrUrl)}" alt="${escapeHtml(t("training.public_profile_qr"))}" />
|
||||
<small>${escapeHtml(new URL(publicProfileUrl).host)}</small>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
<small class="training-profile-qr-hint">${escapeHtml(t("training.public_profile_qr_hint"))}</small>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="trainingPublicProfileCopy" type="button">${escapeHtml(t("training.copy_link"))}</button>
|
||||
<button class="btn" id="trainingPublicProfileOpen" type="button">${escapeHtml(t("training.open_profile"))}</button>
|
||||
@@ -668,6 +700,31 @@ export function renderTrainingView(deps) {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("trainingPublicBaseSave")?.addEventListener("click", () => {
|
||||
const input = document.getElementById("trainingPublicBaseUrl");
|
||||
const normalized = normalizePublicBaseUrl(input?.value);
|
||||
if (input?.value && !normalized) {
|
||||
document.getElementById("trainingPublicAddressStatus").textContent = t("training.public_url_invalid");
|
||||
return;
|
||||
}
|
||||
state.settings.publicBaseUrl = normalized;
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
document.getElementById("trainingPublicAddressDetect")?.addEventListener("click", async () => {
|
||||
const status = document.getElementById("trainingPublicAddressStatus");
|
||||
if (status) status.textContent = t("training.detecting_public_ip");
|
||||
try {
|
||||
const response = await fetch("/api/network/public-address", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
const input = document.getElementById("trainingPublicBaseUrl");
|
||||
if (input) input.value = payload.suggestedBaseUrl || "";
|
||||
if (status) status.textContent = t("training.detected_public_ip", { address: payload.address || "-" });
|
||||
} catch {
|
||||
if (status) status.textContent = t("training.detect_public_ip_failed");
|
||||
}
|
||||
});
|
||||
document.getElementById("trainingPublicProfileEnable")?.addEventListener("click", () => {
|
||||
replacePublicProfileShare(state, selectedProfileId, profileReferences);
|
||||
saveState();
|
||||
@@ -928,12 +985,29 @@ function formatTrainingHighlight(highlight, { t, formatLap }) {
|
||||
return t("training.highlight_today_laps", { laps: highlight.value });
|
||||
}
|
||||
|
||||
function buildPublicProfileUrl(token, language) {
|
||||
const url = new URL(`/public-training/${encodeURIComponent(token)}`, window.location.origin);
|
||||
function normalizePublicBaseUrl(value) {
|
||||
try {
|
||||
const url = new URL(String(value || "").trim());
|
||||
return ["http:", "https:"].includes(url.protocol) ? url.origin : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function buildPublicProfileUrl(token, language, publicBaseUrl = "") {
|
||||
const url = new URL(`/public-training/${encodeURIComponent(token)}`, publicBaseUrl || window.location.origin);
|
||||
url.searchParams.set("lang", language === "en" ? "en" : "sv");
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function buildPublicProfileQrUrl(token, language, baseUrl) {
|
||||
const params = new URLSearchParams({
|
||||
lang: language === "en" ? "en" : "sv",
|
||||
baseUrl,
|
||||
});
|
||||
return `/public-training/qr/${encodeURIComponent(token)}?${params.toString()}`;
|
||||
}
|
||||
|
||||
function generateShareToken() {
|
||||
const bytes = new Uint8Array(24);
|
||||
crypto.getRandomValues(bytes);
|
||||
|
||||
Reference in New Issue
Block a user