From 1a6e533a9ade7c0c0a79fe4e501d7ed42b2ae6c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 17:59:23 +0000 Subject: [PATCH] Ticket > Details > Projekte: geheimer Vorschau-Link (erstellen, kopieren, oeffnen, neu, abschalten) Co-Authored-By: Claude Opus 5.5 --- src/components/AssignedProjectCard.jsx | 3 + src/components/VorschauLinkRow.jsx | 85 ++++++++++++++++++++++++++ src/lib/projectAdminApi.js | 9 +++ src/lib/vorschauLink.js | 10 +++ src/styles/global.css | 6 ++ tests/unit/vorschauLink.test.mjs | 19 ++++++ 6 files changed, 132 insertions(+) create mode 100644 src/components/VorschauLinkRow.jsx create mode 100644 src/lib/vorschauLink.js create mode 100644 tests/unit/vorschauLink.test.mjs diff --git a/src/components/AssignedProjectCard.jsx b/src/components/AssignedProjectCard.jsx index d40973d..e9c8da8 100644 --- a/src/components/AssignedProjectCard.jsx +++ b/src/components/AssignedProjectCard.jsx @@ -4,6 +4,7 @@ import PreviewBlock from './PreviewBlock' import AnalyseRow from './AnalyseRow' import GiteaLinkButton from './GiteaLinkButton' import PreviewLinkButton from './PreviewLinkButton' +import VorschauLinkRow from './VorschauLinkRow' import { statusValueTone } from './projectTones' import { suggestSubdomain } from '../lib/subdomain' import { needsPreviewButton, assignSummary } from '../lib/projectAssign' @@ -87,6 +88,8 @@ export default function AssignedProjectCard({ project, onUnassign, showActions = {project.subdomain && } + {showActions && project.subdomain && } + {showActions && project.subdomain && ( <> + Öffnen + + + + + ) : ( +
+ +
+ )} + {message &&

{message}

} + + ) +} diff --git a/src/lib/projectAdminApi.js b/src/lib/projectAdminApi.js index fc8f06f..99f73ba 100644 --- a/src/lib/projectAdminApi.js +++ b/src/lib/projectAdminApi.js @@ -57,6 +57,15 @@ export async function updateWebsiteProject(id, payload) { }) } +// Geheimer Vorschau-Link: neu erzeugen (der alte wird ungültig) bzw. abschalten +export async function erzeugeVorschauLink(id) { + return adminFetch(`/api/admin/website-projects/${id}/vorschau-link`, { method: 'POST' }) +} + +export async function schalteVorschauLinkAb(id) { + return adminFetch(`/api/admin/website-projects/${id}/vorschau-link`, { method: 'DELETE' }) +} + export async function archiveWebsiteProject(id) { return adminFetch(`/api/admin/website-projects/${id}/archive`, { method: 'POST' }) } diff --git a/src/lib/vorschauLink.js b/src/lib/vorschauLink.js new file mode 100644 index 0000000..6653caa --- /dev/null +++ b/src/lib/vorschauLink.js @@ -0,0 +1,10 @@ +// Geheimer Vorschau-Link (2026-10-26): wer ihn kennt, sieht die Preview ohne Login. +// Das Portal löst ihn ein (Cookie + Weiterleitung), siehe Kundenportal server/services/vorschauLink.js. +const PORTAL = 'https://project.webklar.com' + +export function vorschauLinkUrl(project, base = PORTAL) { + const subdomain = String(project?.subdomain || '').trim().toLowerCase() + const token = String(project?.shareToken || '').trim() + if (!subdomain || !token) return '' + return `${String(base).replace(/\/$/, '')}/vorschau/${encodeURIComponent(subdomain)}/${encodeURIComponent(token)}` +} diff --git a/src/styles/global.css b/src/styles/global.css index 535f31b..a3cf2ad 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1112,6 +1112,12 @@ button.link { text-align: left; } /* Reset und Gewicht von button.link stehen im .proj-head { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 16px; } .proj-main { min-width: 0; overflow-wrap: anywhere; } .proj-meta { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 12px; color: var(--text-2); } +/* Geheimer Vorschau-Link: Feld volle Breite, Aktionen darunter */ +.vorschau-link { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; } +.vorschau-link-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; } +.vorschau-link-label { font-size: 13px; font-weight: 600; } +.vorschau-link-url { width: 100%; min-width: 0; font-size: 13px; } +.vorschau-link .msg { margin: 0; } .ticket-projects-assign { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; } .ticket-projects-assign .select { flex: 1 1 220px; min-width: 0; } .ticket-projects-assign .input { flex: 1 1 160px; min-width: 0; } diff --git a/tests/unit/vorschauLink.test.mjs b/tests/unit/vorschauLink.test.mjs new file mode 100644 index 0000000..0b2b8ae --- /dev/null +++ b/tests/unit/vorschauLink.test.mjs @@ -0,0 +1,19 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { loadModule } from './load.mjs' + +const { vorschauLinkUrl } = await loadModule('src/lib/vorschauLink.js') + +test('vorschauLinkUrl: Link aus Subdomain und Token', () => { + assert.equal( + vorschauLinkUrl({ subdomain: 'Doener-Nation', shareToken: 'abcDEF_123-xyzxyzxyzxyz' }), + 'https://project.webklar.com/vorschau/doener-nation/abcDEF_123-xyzxyzxyzxyz' + ) + assert.equal(vorschauLinkUrl({ subdomain: 'a', shareToken: 't' }, 'http://x/'), 'http://x/vorschau/a/t') +}) + +test('vorschauLinkUrl: ohne Token oder Subdomain kein Link', () => { + assert.equal(vorschauLinkUrl({ subdomain: 'a', shareToken: '' }), '') + assert.equal(vorschauLinkUrl({ subdomain: '', shareToken: 'x' }), '') + assert.equal(vorschauLinkUrl(null), '') +})