Workers P2: Seite /hustler (lazy), Menü „Hustler“ statt Leads, Stil und Schrift Pixelify Sans
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6f1b0c56d
commit
7ddd60e59b
10 files changed
+382
-7
No files matched your search
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Pixelify Sans Project Authors (https://github.com/eifetx/Pixelify-Sans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
+13
-2
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react'
|
||||
import { BrowserRouter, Routes, Route, Navigate, Link, useLocation } from 'react-router-dom'
|
||||
import { AuthProvider, useAuth } from './context/AuthContext'
|
||||
import Sidebar from './components/Navbar'
|
||||
@@ -25,6 +25,9 @@ import BankCallbackPage from './pages/BankCallbackPage'
|
||||
import AdminRoute from './components/AdminRoute'
|
||||
import { safeReturnPath } from './lib/returnPath'
|
||||
|
||||
// Hochhaus der Hustler: erst beim Öffnen laden, damit Engine und Schrift nicht im Haupt-Bundle liegen
|
||||
const WorkersPage = lazy(() => import('./pages/WorkersPage'))
|
||||
|
||||
function ProtectedRoute({ children }) {
|
||||
const { user, loading } = useAuth()
|
||||
const location = useLocation()
|
||||
@@ -40,6 +43,12 @@ function ProtectedRoute({ children }) {
|
||||
return children
|
||||
}
|
||||
|
||||
// Alte Adressen /leads und /workers führen auf /hustler, die Query bleibt (/workers?tab=botleads&bot=<id> wird /hustler?tab=botleads&bot=<id>)
|
||||
function HustlerRedirect() {
|
||||
const { search } = useLocation()
|
||||
return <Navigate to={`/hustler${search}`} replace />
|
||||
}
|
||||
|
||||
function AppRoutes() {
|
||||
const { user } = useAuth()
|
||||
const location = useLocation()
|
||||
@@ -57,7 +66,9 @@ function AppRoutes() {
|
||||
<Route path="/customers/gutschriften" element={<ProtectedRoute><CreditsPage /></ProtectedRoute>} />
|
||||
<Route path="/customers/angebote" element={<ProtectedRoute><OffersPage /></ProtectedRoute>} />
|
||||
<Route path="/customers/:id" element={<ProtectedRoute><CustomerDetailPage /></ProtectedRoute>} />
|
||||
<Route path="/leads" element={<Navigate to="/tickets" replace />} />
|
||||
<Route path="/hustler" element={<ProtectedRoute><Suspense fallback={<div className="loading">Wird geladen …</div>}><WorkersPage /></Suspense></ProtectedRoute>} />
|
||||
<Route path="/leads" element={<HustlerRedirect />} />
|
||||
<Route path="/workers" element={<HustlerRedirect />} />
|
||||
{/* Finanzen: nur Admins, alle anderen gehen zu /tickets */}
|
||||
<Route path="/finance" element={<ProtectedRoute><AdminRoute><FinancePage /></AdminRoute></ProtectedRoute>} />
|
||||
<Route path="/finance/einstellungen" element={<ProtectedRoute><AdminRoute><FinanceSettingsPage /></AdminRoute></ProtectedRoute>} />
|
||||
|
||||
+1
-2
@@ -19,8 +19,7 @@ export function buildNavLinks({ isAdmin, pulseLevel, counts = {} }) {
|
||||
{ key: 'speicher', label: 'E-Mail-Speicher', href: '/customers/email-speicher', count: counts.storage ?? 0 },
|
||||
],
|
||||
},
|
||||
// Leads liegen auf Eis (Lead-Automatik gestoppt 2026-09-30): grau, nicht klickbar
|
||||
{ key: 'leads', label: 'Leads', href: '/leads', paused: true },
|
||||
{ key: 'hustler', label: 'Hustler', href: '/hustler' },
|
||||
...(isAdmin ? [{ key: 'finance', label: 'Finanzen', href: '/finance', also: '/finanzen' }] : []),
|
||||
{ key: 'legal', label: 'Rechtliches', href: '/rechtliches' },
|
||||
...(isAdmin ? [{ key: 'serverpuls', label: 'Serverpuls', href: '/serverpuls', dot: pulseLevel }] : []),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { PageHeader, Tabs } from '../components/ui'
|
||||
import WorkersBoard from '../components/workers/WorkersBoard'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { workersApi } from '../lib/workersClient'
|
||||
import { pickTab } from '../lib/workersView'
|
||||
import '../styles/workers.css'
|
||||
|
||||
// Reiter der Seite. Später kommen freigabe (P3) und botleads (P6) dazu; die Reiterleiste erscheint erst ab zwei Reitern.
|
||||
const WORKERS_TABS = [
|
||||
{ id: 'hochhaus', label: 'Hochhaus' },
|
||||
]
|
||||
|
||||
const api = workersApi
|
||||
|
||||
// Seite „Hustler“ (Dateiname bleibt WorkersPage): Titel, Reiterwahl über ?tab= und der Inhalt des gewählten Reiters.
|
||||
// Auth und Dienst-API kommen von hier; das Board selbst kennt weder Anmeldung noch Appwrite.
|
||||
export default function WorkersPage() {
|
||||
const { isAdmin } = useAuth()
|
||||
const [params, setParams] = useSearchParams()
|
||||
const active = pickTab(WORKERS_TABS, params.get('tab'))
|
||||
|
||||
// Tab-Titel nur solange die Seite offen ist; beim Verlassen kommt der vorige Titel zurück
|
||||
useEffect(() => {
|
||||
const previous = document.title
|
||||
document.title = 'Hustler'
|
||||
return () => { document.title = previous }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="page wk-page">
|
||||
<PageHeader title="Hustler" subtitle="Die Nachtschicht deiner Claude-Armee" />
|
||||
{WORKERS_TABS.length > 1 && (
|
||||
<Tabs tabs={WORKERS_TABS} active={active.id} onChange={(id) => setParams({ tab: id })} />
|
||||
)}
|
||||
{active.id === 'hochhaus' && <WorkersBoard api={api} isAdmin={isAdmin} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/* Hustler (Workers P2): Hochhaus, Kennzahlen, Worker-Karte, Liste, Einstellungen. Weißes Theme: Haarlinien statt Karten, nur Tokens aus global.css. */
|
||||
|
||||
/* Schrift selbst gehostet (SIL OFL 1.1, Lizenztext liegt daneben), nur der Latin-Schnitt mit allen Gewichten */
|
||||
@font-face {
|
||||
font-family: "Pixelify Sans";
|
||||
src: url("/fonts/PixelifySans-Variable.woff2") format("woff2");
|
||||
font-weight: 400 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.wk-page .page-head h1 { font-family: "Pixelify Sans", var(--font); letter-spacing: 0; }
|
||||
|
||||
/* ---------- Kopfzeile mit Pausenschalter ---------- */
|
||||
|
||||
.wk-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
border-bottom: 1px solid var(--line);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.wk-status { color: var(--text-2); }
|
||||
|
||||
/* ---------- Kennzahlen ---------- */
|
||||
|
||||
.wk-kpis.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 16px 32px;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
.wk-kpis .stat { min-width: 0; font-variant-numeric: tabular-nums; }
|
||||
.wk-kpis .stat .sub { min-height: 16px; }
|
||||
|
||||
/* ---------- Hochhaus und Seitenspalte ---------- */
|
||||
|
||||
.wk-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
}
|
||||
.wk-side {
|
||||
min-width: 0;
|
||||
padding-left: 24px;
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.wk-stage { position: relative; min-width: 0; }
|
||||
.wk-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 5 / 4;
|
||||
image-rendering: pixelated;
|
||||
cursor: pointer;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
.wk-zoom {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.wk-zoom .btn { min-width: 32px; }
|
||||
|
||||
/* ---------- Worker-Karte ---------- */
|
||||
|
||||
.wk-card { position: relative; padding-bottom: 20px; }
|
||||
.wk-card > .btn-quiet { position: absolute; top: 0; right: 0; }
|
||||
.wk-card h3 { margin: 20px 0 8px; font-size: 13px; line-height: 20px; font-weight: 600; color: var(--text-2); }
|
||||
.wk-portrait {
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 96px;
|
||||
margin-bottom: 8px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
.wk-name { font-family: "Pixelify Sans", var(--font); font-size: 20px; line-height: 26px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.wk-role { color: var(--text-2); margin-bottom: 8px; }
|
||||
.wk-hint { color: var(--text-2); font-size: 13px; }
|
||||
|
||||
.wk-chip {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
padding: 0 6px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
background: var(--grey-bg);
|
||||
color: var(--grey-fg);
|
||||
}
|
||||
.wk-chip-opus { background: var(--orange-bg); color: var(--orange-fg); }
|
||||
.wk-chip-sonnet { background: var(--blue-bg); color: var(--blue-fg); }
|
||||
.wk-chip-haiku { background: var(--green-bg); color: var(--green-fg); }
|
||||
.wk-chip-overseer { background: var(--violet-bg); color: var(--violet-fg); }
|
||||
|
||||
.wk-facts, .wk-counters dl { margin: 12px 0 0; }
|
||||
.wk-facts > div, .wk-counters dl > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 6px 0;
|
||||
border-top: 1px solid var(--line);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.wk-facts dt, .wk-counters dt { color: var(--text-2); flex: none; }
|
||||
.wk-facts dd, .wk-counters dd { text-align: right; min-width: 0; overflow-wrap: anywhere; }
|
||||
|
||||
.wk-trace { list-style: none; margin: 0; padding: 0; }
|
||||
.wk-trace li { padding: 6px 0; border-top: 1px solid var(--line); font-size: 13px; line-height: 18px; overflow-wrap: anywhere; }
|
||||
.wk-trace-time { color: var(--text-3); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---------- Liste aller Worker ---------- */
|
||||
|
||||
.wk-list { margin-top: 8px; border-top: 1px solid var(--line); }
|
||||
.wk-list ul { list-style: none; margin: 0; padding: 0; }
|
||||
.wk-list-floor { margin: 16px 0 4px; font-size: 12px; line-height: 16px; font-weight: 600; color: var(--text-3); }
|
||||
.wk-list button {
|
||||
display: grid;
|
||||
grid-template-columns: 8px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius);
|
||||
text-align: left;
|
||||
transition: background-color 0.12s;
|
||||
}
|
||||
.wk-list button:hover { background: var(--bg-hover); }
|
||||
.wk-list button[aria-pressed="true"] { background: var(--accent-soft); font-weight: 600; }
|
||||
.wk-list button > span:last-child { color: var(--text-2); font-size: 12px; font-weight: 400; }
|
||||
.wk-list button:focus-visible { outline-offset: -2px; }
|
||||
|
||||
.wk-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-3); }
|
||||
.wk-dot-arbeitet { background: var(--green-fg); }
|
||||
.wk-dot-wartet { background: var(--nav-dot); }
|
||||
.wk-dot-leerlauf { background: var(--text-3); }
|
||||
.wk-dot-grau { background: transparent; box-shadow: inset 0 0 0 1.5px var(--line-strong); }
|
||||
.wk-dot-fehler { background: var(--danger); }
|
||||
|
||||
/* ---------- Einstellungen (Dialog) ---------- */
|
||||
|
||||
.wk-set-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.wk-set-title { font-size: 13px; line-height: 20px; font-weight: 600; color: var(--text-2); }
|
||||
.wk-set-group .muted { font-size: 13px; }
|
||||
|
||||
.wk-halt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--danger);
|
||||
border-bottom: 1px solid var(--danger);
|
||||
}
|
||||
.wk-halt .wk-set-title { color: var(--red-fg); }
|
||||
|
||||
/* ---------- Schmale Bildschirme ---------- */
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.wk-kpis.stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.wk-main { grid-template-columns: minmax(0, 1fr); gap: 24px; }
|
||||
.wk-side { padding-left: 0; border-left: 0; padding-top: 24px; border-top: 1px solid var(--line); }
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.wk-kpis.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; }
|
||||
.wk-zoom { gap: 6px; }
|
||||
.wk-zoom .btn { min-width: 36px; min-height: 36px; }
|
||||
.wk-toolbar .btn { min-height: 36px; }
|
||||
}
|
||||
|
||||
/* ---------- Weniger Bewegung ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wk-list button, .wk-zoom .btn, .wk-toolbar .btn { transition: none; }
|
||||
}
|
||||
@@ -15,11 +15,12 @@ test('buildNavLinks: vier Unterpunkte unter Kunden mit Zahlen, Admin-Eintraege n
|
||||
['angebote', 'Abo-Angebote', '/customers/angebote', undefined],
|
||||
['speicher', 'E-Mail-Speicher', '/customers/email-speicher', 2],
|
||||
])
|
||||
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'legal'])
|
||||
assert.equal(links.find((l) => l.key === 'leads').paused, true)
|
||||
assert.deepEqual(links.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'hustler', 'legal'])
|
||||
assert.deepEqual(links.find((l) => l.key === 'hustler'), { key: 'hustler', label: 'Hustler', href: '/hustler' })
|
||||
assert.ok(!links.some((l) => l.key === 'leads' || l.key === 'workers'))
|
||||
assert.equal(kunden.children.find((c) => c.key === 'chat').hot, true)
|
||||
const admin = buildNavLinks({ isAdmin: true, pulseLevel: 'warn', counts: {} })
|
||||
assert.deepEqual(admin.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'leads', 'finance', 'legal', 'serverpuls', 'admin'])
|
||||
assert.deepEqual(admin.map((l) => l.key), ['tickets', 'planboard', 'projects', 'services', 'customers', 'hustler', 'finance', 'legal', 'serverpuls', 'admin'])
|
||||
assert.equal(admin.find((l) => l.key === 'serverpuls').dot, 'warn')
|
||||
assert.equal(admin.find((l) => l.key === 'finance').also, '/finanzen')
|
||||
assert.equal(admin.find((l) => l.key === 'customers').children[0].count, 0, 'ohne Zahl 0')
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
|
||||
test('workers.css: nur wk-Klassen, @font-face, keine Hex-Farben, reduzierte Bewegung', () => {
|
||||
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '') // Kommentare vorher entfernen
|
||||
for (const m of css.matchAll(/([^{}]+)\{/g)) { const pre = m[1].trim(); if (pre.startsWith('@')) continue; for (const sel of pre.split(',')) assert.ok(sel.trim().startsWith('.wk-'), sel) } // jede Regel beginnt mit einer wk-Klasse (Nachfahren dürfen globale Klassen nutzen)
|
||||
assert.match(css, /@font-face\s*\{[^}]*font-family:\s*"Pixelify Sans"[^}]*PixelifySans-Variable\.woff2[^}]*font-display:\s*swap/s); assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/)
|
||||
assert.match(css, /prefers-reduced-motion/); assert.match(css, /@media \(max-width: 1100px\)/); assert.match(css, /@media \(max-width: 860px\)/)
|
||||
})
|
||||
test('jede im JSX verwendete wk-Klasse ist definiert', () => {
|
||||
const css = readFileSync(new URL('../../src/styles/workers.css', import.meta.url), 'utf8'); const dir = new URL('../../src/components/workers/', import.meta.url)
|
||||
for (const f of readdirSync(dir).filter((x) => x.endsWith('.jsx'))) for (const m of readFileSync(new URL(f, dir), 'utf8').matchAll(/(?<![\w-])wk-[a-z0-9-]+/g)) assert.ok(css.includes('.' + m[0]), `${f}: ${m[0]}`) // Lookbehind: das Attribut data-wk-worker ist keine Klasse
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
test('Schrift: woff2-Magic-Bytes, plausible Größe, OFL-Lizenztext', () => {
|
||||
const buf = readFileSync(new URL('../../public/fonts/PixelifySans-Variable.woff2', import.meta.url)); assert.equal(buf.subarray(0, 4).toString('latin1'), 'wOF2'); assert.ok(buf.length > 10000 && buf.length < 400000, String(buf.length))
|
||||
const lic = readFileSync(new URL('../../public/fonts/OFL-PixelifySans.txt', import.meta.url), 'utf8'); assert.match(lic, /SIL OPEN FONT LICENSE Version 1\.1/); assert.match(lic, /Pixelify Sans/)
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
// Die Seite und die Routen sind unter Node nicht ladbar (JSX, Router): geprüft wird der Quelltext (Muster workersComponents.test.mjs)
|
||||
test('App: /hustler lazy hinter ProtectedRoute, /leads und /workers leiten mit erhaltener Query auf /hustler um, kein statischer Import', () => {
|
||||
const s = readFileSync(new URL('../../src/App.jsx', import.meta.url), 'utf8')
|
||||
assert.match(s, /const WorkersPage = lazy\(\(\) => import\('\.\/pages\/WorkersPage'\)\)/); assert.doesNotMatch(s, /import WorkersPage from/)
|
||||
assert.match(s, /<Route path="\/hustler" element=\{<ProtectedRoute><Suspense[\s\S]*?<WorkersPage \/><\/Suspense><\/ProtectedRoute>\} \/>/)
|
||||
assert.match(s, /<Route path="\/leads" element=\{<HustlerRedirect \/>\} \/>/); assert.match(s, /<Route path="\/workers" element=\{<HustlerRedirect \/>\} \/>/)
|
||||
assert.match(s, /function HustlerRedirect\(\)[\s\S]*?useLocation\(\)[\s\S]*?<Navigate to=\{`\/hustler\$\{search\}`\} replace \/>/) // /workers?tab=botleads&bot=x → /hustler?tab=botleads&bot=x
|
||||
assert.ok(s.indexOf('path="/hustler"') < s.indexOf('path="*"')); assert.ok(s.indexOf('path="/workers"') < s.indexOf('path="*"'))
|
||||
})
|
||||
test('WorkersPage: Titel Hustler (H1 und Tab-Titel), Reiter hochhaus (kein Platzhalter), Reiterleiste erst ab zwei Reitern, api-Instanz, Admin-Weitergabe', () => {
|
||||
const s = readFileSync(new URL('../../src/pages/WorkersPage.jsx', import.meta.url), 'utf8')
|
||||
assert.match(s, /title="Hustler"/); assert.match(s, /document\.title = 'Hustler'/)
|
||||
assert.match(s, /id: 'hochhaus'/); assert.doesNotMatch(s, /id: 'freigabe'/); assert.match(s, /WORKERS_TABS\.length > 1/)
|
||||
assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(/); assert.match(s, /isAdmin=\{isAdmin\}/)
|
||||
})
|
||||
Reference in new issue
Block a user