Projekte: Services als untereinander stehende Abschnitte mit Zeilen, Preview-Zustand als Badge
Das Kachelraster entfaellt, jede Art ist ein Abschnitt mit Beschreibung, Funktions-Badges und Zeilen. Der Preview-Punkt wird zum Badge "Preview: <Zustand>", Ticket und Zuordnen sind Textlinks. Zwei CSS-Zeilen (.services-stack) im Block Projekte (P5). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b688f9df9
commit
816d4552fa
2 files changed
+45
-43
No files matched your search
+41
-43
@@ -1,14 +1,12 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { FaSpinner } from 'react-icons/fa6'
|
||||
import { dotColor } from '../lib/previewEnv'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { fetchServices } from '../lib/projectKindsApi'
|
||||
import { servicesCards } from '../lib/servicesView'
|
||||
import { ticketPath } from '../lib/ticketRoutes'
|
||||
import { previewStateTone } from '../components/projectTones'
|
||||
import { PageHeader, Card, Button, Badge } from '../components/ui'
|
||||
|
||||
export default function ServicesPage() {
|
||||
const navigate = useNavigate()
|
||||
const [data, setData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
@@ -28,59 +26,59 @@ export default function ServicesPage() {
|
||||
|
||||
const cards = useMemo(() => servicesCards(data), [data])
|
||||
|
||||
if (loading) return <div className="page"><div className="empty"><FaSpinner className="spinner" /></div></div>
|
||||
if (loading) return <div className="page"><div className="loading">Wird geladen …</div></div>
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader title="Services" subtitle="Unsere Projekte nach Art" />
|
||||
{error && (
|
||||
<Card>
|
||||
<div className="text-red" style={{ marginBottom: 8 }}>{error}</div>
|
||||
<Button onClick={load}>Erneut versuchen</Button>
|
||||
</Card>
|
||||
<div className="services-stack">
|
||||
<p className="msg err" role="alert">{error}</p>
|
||||
<div className="row-buttons">
|
||||
<Button type="button" onClick={load}>Erneut versuchen</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 320px), 1fr))', gap: 12 }}>
|
||||
{cards.map((c) => (
|
||||
<Card key={c.key} title={`${c.label} (${c.count})`}>
|
||||
{c.description && <div className="muted" style={{ marginBottom: 8 }}>{c.description}</div>}
|
||||
{cards.map((c) => (
|
||||
<Card key={c.key} title={`${c.label} (${c.count})`}>
|
||||
<div className="services-stack">
|
||||
{c.description && <p className="muted">{c.description}</p>}
|
||||
{c.features.length > 0 && (
|
||||
<div className="flex gap-2 wrap" style={{ marginBottom: 8 }}>
|
||||
<div className="pv-row">
|
||||
{c.features.map((f) => (
|
||||
<Badge key={f.label}>{f.label}{f.statusLabel ? ` · ${f.statusLabel}` : ''}</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="list">
|
||||
{c.projects.map((p) => (
|
||||
<div key={p.id} className="list-row" style={{ flexWrap: 'wrap' }}>
|
||||
<div className="grow" style={{ minWidth: 0, overflowWrap: 'anywhere' }}>
|
||||
<div style={{ fontWeight: 700 }}>
|
||||
{p.previewState && (
|
||||
<span
|
||||
title={`Preview: ${p.previewState}`}
|
||||
style={{ display: 'inline-block', width: 9, height: 9, borderRadius: '50%', background: dotColor(p.previewState), marginRight: 6 }}
|
||||
>
|
||||
<span style={{ position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap' }}>{`Preview: ${p.previewState}`}</span>
|
||||
</span>
|
||||
)}
|
||||
{p.projectName || p.repoFullName}
|
||||
{p.features?.analyse === 'active' && <Badge>Analyse an</Badge>}
|
||||
</div>
|
||||
<div className="muted">
|
||||
{[p.customerName, p.previewUrl && 'Preview'].filter(Boolean).join(' · ') || 'Intern'}
|
||||
{c.projects.length > 0 ? (
|
||||
<div className="list">
|
||||
{c.projects.map((p) => (
|
||||
<div key={p.id} className="list-row">
|
||||
<div className="list-row-text">
|
||||
<div className="cell-title">
|
||||
{p.projectName || p.repoFullName}
|
||||
{p.previewState && <Badge tone={previewStateTone(p.previewState)}>Preview: {p.previewState}</Badge>}
|
||||
{p.features?.analyse === 'active' && <Badge>Analyse an</Badge>}
|
||||
</div>
|
||||
<div className="cell-sub">
|
||||
{[p.customerName, p.previewUrl && 'Preview'].filter(Boolean).join(' · ') || 'Intern'}
|
||||
</div>
|
||||
</div>
|
||||
{(p.woid || c.key === 'missing') && (
|
||||
<div className="row-actions">
|
||||
{p.woid && <Link className="link" to={ticketPath(p.woid)}>Ticket #{p.woid}</Link>}
|
||||
{c.key === 'missing' && <Link className="link" to={`/projects/zuordnen/${p.id}`}>Zuordnen</Link>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{p.woid && <Link to={ticketPath(p.woid)}>Ticket #{p.woid}</Link>}
|
||||
{c.key === 'missing' && (
|
||||
<Button variant="primary" onClick={() => navigate(`/projects/zuordnen/${p.id}`)}>Zuordnen</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{c.projects.length === 0 && <div className="muted" style={{ padding: 12 }}>Noch keine Projekte.</div>}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted pick-empty">Noch keine Projekte.</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1188,6 +1188,10 @@ button.list-row:focus-visible { outline-offset: -2px; } /* in .pick-list (Scroll
|
||||
.assign-step > .seg, .assign-step > .check { align-self: flex-start; }
|
||||
.page .assign-step .field { margin-bottom: 0; } /* KOMPAT gibt .page .field 12 px unten; hier steht der Abstand der Spalte (P10 loescht beide) */
|
||||
button.list-row:hover .faint, button.list-row[aria-pressed="true"] .faint { color: var(--text-2); } /* .faint hat auf --bg-hover nur 2,9:1 (Muster: .table tr.row:hover .faint) */
|
||||
/* ---------- Services ---------- */
|
||||
/* Beschreibung, Funktions-Badges und Zeilen einer Art als Spalte mit Abstand (der Reset setzt jeden Aussenabstand auf 0); auch Fehlermeldung plus Knopf */
|
||||
.services-stack { display: flex; flex-direction: column; gap: 12px; }
|
||||
.services-stack > .pick-empty { padding: 0; } /* der Abstand kommt aus gap */
|
||||
/* ===== ENDE Projekte (P5) ===== */
|
||||
|
||||
/* ===== KOMPAT (bis P10) ===== */
|
||||
|
||||
Reference in new issue
Block a user