feat: oversize sheets, image duplication, free labelling and a new layout
Prints larger than the sheet no longer just fail. Each cell can now spill over the whole sheet (20x30 on A4 loses exactly 3 mm, and says so beforehand) or be tiled across several sheets with a dashed glue fold and a sheet number. The maths lives in printoversize.ts, dependency-free, so the browser preview and the server PDF compute the same thing -- including the note strip that keeps the sheet number off the picture. The same image can now appear as several cells, so a portrait can be printed at two sizes on one sheet without copying and re-uploading the file. The footer line can be switched off, given custom text and placed at one of six positions; it is only drawn where that edge is free. Layout reworked for all three widths: work left, sticky preview right on the desktop, collapsed setting groups paired in a two-column grid that expands full width; preview first and a fixed action bar on the phone. Verified at 390, 820, 1440 and 1680 px -- no horizontal overflow, no dead white space. Also a public presentation page at /vorstellung (no login, nothing operable), whose sample sheet is computed by the real packer rather than drawn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
---
|
||||
// Öffentliche Vorstellungsseite für das Druckmodul.
|
||||
// Bewusst ohne Login und ohne Bedienelemente: Sie zeigt, was das Werkzeug kann,
|
||||
// man kann es hier aber nicht benutzen. Gedacht als Landeseite für ein
|
||||
// Open-Source-Release (Gitea/GitHub) und zum Herzeigen.
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { PHOTO_SIZES, PAPERS } from '../lib/paper';
|
||||
import { layout, cutMarks, type SheetSpec } from '../lib/printlayout';
|
||||
|
||||
const anzahlFormate = PHOTO_SIZES.length;
|
||||
const anzahlPapiere = PAPERS.length;
|
||||
const anzahlGruppen = new Set(PHOTO_SIZES.map((p) => p.group)).size;
|
||||
|
||||
const koennen = [
|
||||
{
|
||||
kicker: 'Maßhaltig',
|
||||
titel: 'Millimeter statt „ungefähr“',
|
||||
text: 'Jedes Endmaß wird in Millimetern gerechnet und erst beim Rendern in Pixel umgesetzt. ' +
|
||||
'Das PDF hat exakt das Bogenmaß, jedes Bild darin exakt sein Zielmaß. Nachgemessen wird nicht geschätzt, ' +
|
||||
'sondern gemessen: Wir rastern die fertigen PDFs mit 254 dpi und zählen die Pixelkanten nach.',
|
||||
beleg: '13 × 18 cm → gemessen 130,1 × 180,1 mm',
|
||||
},
|
||||
{
|
||||
kicker: 'Platz',
|
||||
titel: 'Mehrere Bilder auf einen Bogen',
|
||||
text: 'Gleich große Bilder kommen in ein exaktes Raster. Bei gemischten Formaten sucht ein Packer ' +
|
||||
'(MaxRects, „Best Short Side Fit“) die Anordnung mit den wenigsten Bogen — und probiert dafür jede ' +
|
||||
'Kombination aus Hoch- und Querlage durch. Ein Passbild-Satz und ein 13 × 18 passen so auf dasselbe Blatt.',
|
||||
beleg: 'Kita-Satz: 9 Teile statt 3 auf einem A4-Bogen',
|
||||
},
|
||||
{
|
||||
kicker: 'Schneiden',
|
||||
titel: 'Schnittmarken wie in der Druckvorstufe',
|
||||
text: 'Feine Eckmarken außerhalb jedes Bildes oder durchgehende Rasterlinien über den ganzen Bogen. ' +
|
||||
'Die Linien werden dort unterbrochen, wo ein anderes Motiv liegt — es wird also nie quer über ein ' +
|
||||
'Bild gezeichnet. Länge und Abstand sind einstellbar, Beschnittzugabe ebenso.',
|
||||
beleg: 'Eckmarken · durchgehendes Raster · Beschnittzugabe 0–10 mm',
|
||||
},
|
||||
{
|
||||
kicker: 'Zu groß',
|
||||
titel: '20 × 30 auf A4 — trotzdem',
|
||||
text: 'Der Alltagsfall: Es soll ein 20 × 30 werden, im Haus liegt aber nur A4-Fotopapier. ' +
|
||||
'Statt „geht nicht“ gibt es zwei Wege. Überstehen legt das Bild mittig aufs ganze Blatt; ' +
|
||||
'was hinausragt, fehlt hinterher — und wie viel das ist, steht vorher da.',
|
||||
beleg: '20 × 30 cm auf A4: es fehlen genau 3 mm in der Höhe',
|
||||
},
|
||||
{
|
||||
kicker: 'Poster',
|
||||
titel: 'Ein Bild, mehrere Blätter',
|
||||
text: 'Der zweite Weg für große Maße: Das Bild wird auf mehrere Blätter verteilt. Die Kacheln ' +
|
||||
'überlappen sich um einen Klebefalz, der auf beiden Nachbarblättern gedruckt und gestrichelt ' +
|
||||
'markiert wird. Jedes Blatt trägt seine Nummer samt Reihe und Spalte.',
|
||||
beleg: '30 × 40 cm auf A4 → 2 × 2 Blätter, 10 mm Klebefalz',
|
||||
},
|
||||
{
|
||||
kicker: 'Zuschnitt',
|
||||
titel: 'Der Ausschnitt bleibt deiner',
|
||||
text: 'Passt das Seitenverhältnis nicht, entscheidest du: zuschneiden oder Rand lassen. ' +
|
||||
'Im Zuschnitt-Fenster verschiebst und zoomst du das Original im Zielrahmen — mit Maus, Finger, ' +
|
||||
'Doppeltipp oder Pfeiltasten. Der Ausschnitt wird relativ gespeichert und überlebt jede Auflösung.',
|
||||
beleg: 'EXIF-Drehung wird vorab eingerechnet — Handyfotos stehen richtig',
|
||||
},
|
||||
];
|
||||
|
||||
const daten = [
|
||||
{ zahl: String(anzahlFormate), label: 'Bildformate', fein: `in ${anzahlGruppen} Gruppen — Passbild bis 100 × 140 cm` },
|
||||
{ zahl: String(anzahlPapiere), label: 'Papierformate', fein: 'DIN A7–A0, Fotopapier, US-Maße, freies Maß' },
|
||||
{ zahl: '0', label: 'KI im Spiel', fein: 'reine Geometrie, kein Modell, kein Upload nach außen' },
|
||||
{ zahl: '1:1', label: 'Ausgabe', fein: 'PDF in Originalgröße, 72–1200 dpi' },
|
||||
];
|
||||
|
||||
// Der Beispielbogen unten wird nicht gezeichnet, sondern gerechnet — mit genau
|
||||
// demselben Packer, den das Werkzeug benutzt. Ändert sich der Algorithmus,
|
||||
// ändert sich das Bild auf dieser Seite mit.
|
||||
const musterBogen: SheetSpec = { wMm: 210, hMm: 297, marginMm: 5, gapMm: 8, bleedMm: 0, center: true };
|
||||
const musterPlan = layout([
|
||||
{ id: 'gross', wMm: 130, hMm: 180, count: 1, allowRotate: true },
|
||||
{ id: 'klein', wMm: 90, hMm: 130, count: 1, allowRotate: true },
|
||||
{ id: 'pass', wMm: 35, hMm: 45, count: 6, allowRotate: true },
|
||||
], musterBogen);
|
||||
const musterSeite = musterPlan.pages[0];
|
||||
const musterMarken = musterSeite ? cutMarks(musterSeite, musterBogen, { mode: 'corner', lengthMm: 3, offsetMm: 1 }) : [];
|
||||
const musterFarbe: Record<string, string> = {
|
||||
gross: 'oklch(45% 0.23 267 / 0.09)', klein: 'oklch(45% 0.23 267 / 0.20)', pass: 'oklch(45% 0.23 267 / 0.34)',
|
||||
};
|
||||
const musterAnzahl = musterSeite ? musterSeite.placements.length : 0;
|
||||
/** Kompaktes Maß fürs Kästchen: unter 10 cm in mm, sonst in cm. */
|
||||
const kurzMass = (w: number, h: number) => w < 100 && h < 100
|
||||
? `${Math.round(w)}×${Math.round(h)}`
|
||||
: `${Math.round(w / 10)}×${Math.round(h / 10)} cm`;
|
||||
|
||||
const schritte = [
|
||||
{ n: '01', t: 'Bilder wählen', d: 'Hochladen, aus der Bibliothek holen oder ein vorhandenes Bild duplizieren, um es in zwei Maßen zu drucken.' },
|
||||
{ n: '02', t: 'Maße setzen', d: 'Je Bild ein Zielmaß, Anzahl, Hoch oder Quer, zuschneiden oder Rand lassen.' },
|
||||
{ n: '03', t: 'Bogen einstellen', d: 'Papier, Schnittmarken, Ränder, Auflösung. Die Vorschau zeigt sofort, wie viele Bogen es werden.' },
|
||||
{ n: '04', t: 'Drucken', d: 'PDF erzeugen und im Druckdialog „Tatsächliche Größe / 100 %“ wählen. Fertig.' },
|
||||
];
|
||||
---
|
||||
<Base title="Klarbild Drucken — Bilder auf exakte Maße bringen">
|
||||
<main class="vs">
|
||||
|
||||
<header class="held">
|
||||
<p class="mono-label">Klarbild · Druckmodul</p>
|
||||
<h1>Bilder auf exakte Maße bringen.<br /><span class="ohne">Ohne KI.</span></h1>
|
||||
<p class="lead">
|
||||
Ein Passbild ist 35 × 45 Millimeter. Ein Rahmen ist 13 × 18 Zentimeter. Beides sind Maße,
|
||||
keine Meinungen — und trotzdem ist es erstaunlich mühsam, ein Foto genau so aus dem Drucker
|
||||
zu bekommen. Dieses Werkzeug macht daraus einen Handgriff: Zielmaß eintragen, Ausschnitt
|
||||
wählen, PDF drucken. Millimetergenau, mit Schnittmarken, mehrere Bilder auf einem Blatt.
|
||||
</p>
|
||||
<div class="held-marken" aria-hidden="true">
|
||||
<svg viewBox="0 0 300 120" class="held-svg">
|
||||
<rect x="8" y="8" width="284" height="104" fill="#FBFBF7" stroke="#D7D6CC" />
|
||||
<g stroke="#1B3BE0" stroke-width="1">
|
||||
<path d="M26 22h-10M22 26v-10" /><path d="M120 22h10M124 26v-10" />
|
||||
<path d="M26 98h-10M22 94v10" /><path d="M120 98h10M124 94v10" />
|
||||
<path d="M156 22h-10M152 26v-10" /><path d="M282 22h-8M278 26v-10" />
|
||||
<path d="M156 98h-10M152 94v10" /><path d="M282 98h-8M278 94v10" />
|
||||
</g>
|
||||
<rect x="26" y="22" width="94" height="76" fill="#EAEAE3" stroke="#D7D6CC" stroke-width="0.5" />
|
||||
<rect x="156" y="22" width="120" height="76" fill="#EAEAE3" stroke="#D7D6CC" stroke-width="0.5" />
|
||||
<text x="30" y="93" font-family="monospace" font-size="7" fill="#6A685D">35 × 45 mm</text>
|
||||
<text x="160" y="93" font-family="monospace" font-size="7" fill="#6A685D">13 × 18 cm</text>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="zahlen" aria-label="Kennzahlen">
|
||||
{daten.map((d) => (
|
||||
<div class="zahl">
|
||||
<b>{d.zahl}</b>
|
||||
<span class="mono-label">{d.label}</span>
|
||||
<p>{d.fein}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section class="problem">
|
||||
<h2>Das Problem</h2>
|
||||
<p>
|
||||
Der übliche Weg führt über ein Layoutprogramm oder über den Druckertreiber, und beide meinen
|
||||
es zu gut: „an Seitengröße anpassen“, „einpassen“, „randlos skalieren“. Am Ende liegt ein
|
||||
Ausdruck vor, der fast stimmt. Für ein Passbild ist „fast“ zu wenig, und für ein
|
||||
4 × 3-Bildchen ein ganzes DIN-A4-Blatt zu verbrauchen, nur weil es sonst nicht in
|
||||
100 % Größe auf die Seite passt, ist schlicht Verschwendung.
|
||||
</p>
|
||||
<p>
|
||||
Klarbild Drucken rechnet stattdessen in Millimetern, packt so viele Bilder auf einen Bogen,
|
||||
wie hineingehen, und legt Schnittmarken dorthin, wo geschnitten wird. Was aus dem Drucker
|
||||
kommt, hat das Maß, das du eingetragen hast.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="muster">
|
||||
<h2>Ein Bogen, gerechnet</h2>
|
||||
<p class="muster-lead">
|
||||
Ein 13 × 18, ein 9 × 13 und sechs Passbilder — auf einem einzigen DIN-A4-Bogen,
|
||||
alle in 100 % Größe, mit Eckmarken zum Schneiden. Diese Zeichnung ist kein Entwurf:
|
||||
Sie stammt aus demselben Packer, der auch das PDF erzeugt.
|
||||
</p>
|
||||
<div class="muster-halter">
|
||||
<svg class="muster-svg" viewBox={`-6 -6 ${musterBogen.wMm + 12} ${musterBogen.hMm + 12}`} role="img"
|
||||
aria-label={`DIN-A4-Bogen mit ${musterAnzahl} Bildern in drei Formaten`}>
|
||||
<rect x="0" y="0" width={musterBogen.wMm} height={musterBogen.hMm} fill="#FBFBF7" stroke="#D7D6CC" stroke-width="0.4" />
|
||||
{musterSeite?.placements.map((pl) => (
|
||||
<g>
|
||||
<rect x={pl.x} y={pl.y} width={pl.w} height={pl.h}
|
||||
fill={musterFarbe[pl.specId] || 'oklch(45% 0.23 267 / 0.14)'}
|
||||
stroke="oklch(45% 0.23 267 / 0.45)" stroke-width="0.3" />
|
||||
<text x={pl.x + pl.w / 2} y={pl.y + pl.h / 2 + 1.6} text-anchor="middle"
|
||||
font-family="monospace" font-size="4" fill="#16150F" opacity="0.62">
|
||||
{kurzMass(pl.w, pl.h)}
|
||||
</text>
|
||||
</g>
|
||||
))}
|
||||
{musterMarken.map((l) => (
|
||||
<line x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2} stroke="#1B3BE0" stroke-width="0.25" />
|
||||
))}
|
||||
</svg>
|
||||
<ul class="muster-liste">
|
||||
<li><i style="background: oklch(45% 0.23 267 / 0.09)"></i> 1 × 13 × 18 cm</li>
|
||||
<li><i style="background: oklch(45% 0.23 267 / 0.20)"></i> 1 × 9 × 13 cm</li>
|
||||
<li><i style="background: oklch(45% 0.23 267 / 0.34)"></i> 6 × 35 × 45 mm</li>
|
||||
<li class="muster-summe">{musterAnzahl} Teile auf {musterPlan.pages.length} Bogen · 5 mm Rand · 8 mm Abstand</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="koennen">
|
||||
<h2>Was es kann</h2>
|
||||
<div class="karten">
|
||||
{koennen.map((k) => (
|
||||
<article class="kk">
|
||||
<p class="mono-label">{k.kicker}</p>
|
||||
<h3>{k.titel}</h3>
|
||||
<p class="kk-text">{k.text}</p>
|
||||
<p class="kk-beleg">{k.beleg}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ablauf">
|
||||
<h2>In vier Schritten</h2>
|
||||
<ol class="schritte">
|
||||
{schritte.map((s) => (
|
||||
<li><span class="mono-label">{s.n}</span><b>{s.t}</b><p>{s.d}</p></li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="technik">
|
||||
<h2>Unter der Haube</h2>
|
||||
<dl>
|
||||
<div><dt>Einheit</dt><dd>Millimeter. Pixel entstehen erst beim Rendern (<code>px = mm / 25,4 × dpi</code>), PDF-Punkte über <code>mm / 25,4 × 72</code>.</dd></div>
|
||||
<div><dt>Rechenkern</dt><dd>Abhängigkeitsfreies TypeScript. Browser-Vorschau und Server-PDF benutzen buchstäblich denselben Code — was du siehst, wird gedruckt.</dd></div>
|
||||
<div><dt>Packen</dt><dd>Exaktes Raster bei gleichen Maßen, MaxRects (Best Short Side Fit) bei gemischten. Bewertet nach wenigsten Bogen, vollster erster Seite, wenigsten Drehungen.</dd></div>
|
||||
<div><dt>Bild</dt><dd>sharp in einer einzigen Kette: EXIF-Drehung, Ausschnitt, Skalierung, Rand. Der Zuschnitt wird relativ (0…1) gespeichert und ist damit auflösungsunabhängig.</dd></div>
|
||||
<div><dt>PDF</dt><dd>pdf-lib. Seitengröße exakt das Bogenmaß, Bilder 1 : 1 platziert, Schnittmarken als Vektorlinien, Klebefalze gestrichelt.</dd></div>
|
||||
<div><dt>Nachweis</dt><dd>Automatische Tests für die Geometrie plus Rasterprobe der fertigen PDFs bei 254 dpi (= 10 px/mm) mit Kantenmessung.</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="offen">
|
||||
<h2>Selbst betreiben</h2>
|
||||
<p>
|
||||
Klarbild läuft auf einem eigenen Server: Astro mit Node, PostgreSQL, eine Warteschlange im
|
||||
selben Prozess, Dateien lokal oder auf S3. Das Druckmodul kommt ohne jeden externen Dienst
|
||||
aus — kein Modell, kein Schlüssel, keine Bilder, die das Haus verlassen. Wer nur drucken
|
||||
will, braucht die KI-Teile gar nicht erst zu konfigurieren.
|
||||
</p>
|
||||
<p class="fein">
|
||||
Diese Seite ist eine Vorstellung, keine Bedienoberfläche — hier lässt sich nichts hochladen
|
||||
und nichts erzeugen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<footer class="fuss">
|
||||
<span class="regmark"></span>
|
||||
<span>Klarbild · Druckmodul</span>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.vs { max-width: 980px; margin: 0 auto; padding: 0 20px 80px; }
|
||||
.vs h2 { font-size: 1.5rem; letter-spacing: -0.01em; margin: 0 0 14px; }
|
||||
.vs section { margin: 64px 0; }
|
||||
|
||||
.held { padding: 72px 0 8px; }
|
||||
.held h1 { font-size: clamp(2.1rem, 6vw, 3.4rem); line-height: 1.08; letter-spacing: -0.03em; margin: 14px 0 20px; }
|
||||
.ohne { color: var(--accent); }
|
||||
.lead { font-size: clamp(1rem, 2.2vw, 1.15rem); color: var(--ink-soft); max-width: 62ch; margin: 0; line-height: 1.65; }
|
||||
.held-marken { margin-top: 34px; }
|
||||
.held-svg { width: 100%; max-width: 420px; height: auto; display: block; }
|
||||
|
||||
.zahlen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px;
|
||||
background: var(--line); border: 1px solid var(--line); }
|
||||
.zahl { background: var(--card); padding: 20px 18px; }
|
||||
.zahl b { display: block; font-size: 2rem; letter-spacing: -0.03em; line-height: 1.1; }
|
||||
.zahl .mono-label { display: block; margin: 6px 0 8px; }
|
||||
.zahl p { margin: 0; font-size: 0.82rem; color: var(--ink-soft); line-height: 1.5; }
|
||||
|
||||
.problem p { max-width: 68ch; color: var(--ink-soft); margin: 0 0 14px; }
|
||||
|
||||
.muster-lead { max-width: 62ch; color: var(--ink-soft); margin: 0 0 24px; }
|
||||
.muster-halter { display: grid; grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
|
||||
gap: 32px; align-items: start; }
|
||||
.muster-svg { width: 100%; height: auto; display: block; }
|
||||
.muster-liste { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px;
|
||||
font-size: 0.9rem; color: var(--ink-soft); }
|
||||
.muster-liste li { display: flex; align-items: center; gap: 10px; }
|
||||
.muster-liste i { width: 22px; height: 22px; border: 1px solid oklch(45% 0.23 267 / 0.45); flex: none; }
|
||||
.muster-summe { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.08em;
|
||||
color: var(--soft); padding-top: 12px; border-top: 1px solid var(--line); }
|
||||
.muster-summe::before { content: none; }
|
||||
|
||||
.karten { display: grid; grid-template-columns: repeat(auto-fit, minmax(290px, 1fr)); gap: 16px; }
|
||||
.kk { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; }
|
||||
.kk h3 { font-size: 1.12rem; margin: 8px 0 10px; letter-spacing: -0.01em; }
|
||||
.kk-text { margin: 0; font-size: 0.92rem; color: var(--ink-soft); line-height: 1.62; }
|
||||
.kk-beleg { margin: 14px 0 0; padding-top: 12px; border-top: 1px solid var(--line);
|
||||
font-family: var(--font-mono); font-size: 0.72rem; color: var(--accent); line-height: 1.5; }
|
||||
|
||||
.schritte { list-style: none; padding: 0; margin: 0; display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
|
||||
.schritte li { border-top: 2px solid var(--ink); padding-top: 12px; }
|
||||
.schritte b { display: block; margin: 8px 0 6px; }
|
||||
.schritte p { margin: 0; font-size: 0.88rem; color: var(--ink-soft); line-height: 1.6; }
|
||||
|
||||
.technik dl { margin: 0; display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); }
|
||||
.technik dl > div { background: var(--card); padding: 14px 18px; display: grid;
|
||||
grid-template-columns: 130px minmax(0, 1fr); gap: 16px; align-items: baseline; }
|
||||
.technik dt { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em;
|
||||
text-transform: uppercase; color: var(--soft); }
|
||||
.technik dd { margin: 0; font-size: 0.9rem; color: var(--ink-soft); line-height: 1.6; }
|
||||
.technik code { font-family: var(--font-mono); font-size: 0.82em; background: var(--paper);
|
||||
border: 1px solid var(--line); border-radius: 3px; padding: 1px 5px; }
|
||||
|
||||
.offen p { max-width: 68ch; color: var(--ink-soft); margin: 0 0 14px; }
|
||||
.offen .fein { font-size: 0.85rem; color: var(--soft); }
|
||||
|
||||
.fuss { display: flex; align-items: center; gap: 10px; padding-top: 28px;
|
||||
border-top: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px;
|
||||
letter-spacing: 0.14em; text-transform: uppercase; color: var(--soft); }
|
||||
|
||||
@media (max-width: 860px) { .zahlen { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 640px) {
|
||||
.vs { padding: 0 16px 64px; }
|
||||
.held { padding: 40px 0 4px; }
|
||||
.vs section { margin: 44px 0; }
|
||||
.technik dl > div { grid-template-columns: 1fr; gap: 6px; }
|
||||
.muster-halter { grid-template-columns: 1fr; gap: 20px; }
|
||||
}
|
||||
</style>
|
||||
</Base>
|
||||
Reference in New Issue
Block a user