fasd
This commit is contained in:
1 parent
5ece51b567
commit
196b37b746
4 files changed
+192
-12
No files matched your search
+1
-1
@@ -13,5 +13,5 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: { inlineStylesheets: 'auto' },
|
build: { inlineStylesheets: 'auto' },
|
||||||
// In development the form posts to the contact handler started with `npm run contact`.
|
// In development the form posts to the contact handler started with `npm run contact`.
|
||||||
vite: { server: { proxy: { '^/(contact(/challenge)?|notify(/verify)?)$': { target: 'http://127.0.0.1:3000', changeOrigin: false } } } },
|
vite: { server: { proxy: { '^/(contact(/challenge)?|notify(/verify)?)(\\?|$)': { target: 'http://127.0.0.1:3000', changeOrigin: false } } } },
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* The seal on the sign-up result pages: a target drawn as a technical line drawing. On load the
|
||||||
|
* corner brackets close in and the result is drawn in the centre: a check when the link verified,
|
||||||
|
* a cross when it did not. Then it stays still.
|
||||||
|
* Geometry is computed at build time; the motion is CSS only and off under reduced motion.
|
||||||
|
*/
|
||||||
|
interface Props { state: 'confirmed' | 'error'; label: string }
|
||||||
|
const { state, label } = Astro.props;
|
||||||
|
|
||||||
|
const C = 200, R = 184, N = 60;
|
||||||
|
const f = (n: number) => Math.round(n * 10) / 10;
|
||||||
|
const at = (r: number, a: number): [number, number] => [C + r * Math.sin(a), C - r * Math.cos(a)];
|
||||||
|
|
||||||
|
/* Ticks clockwise from twelve o'clock; every fifth is long, so the ring reads as a scale. */
|
||||||
|
const ticks = Array.from({ length: N }, (_, i) => {
|
||||||
|
const a = (i / N) * Math.PI * 2;
|
||||||
|
const [x1, y1] = at(R, a);
|
||||||
|
const [x2, y2] = at(R - (i % 5 === 0 ? 20 : 10), a);
|
||||||
|
return { x1: f(x1), y1: f(y1), x2: f(x2), y2: f(y2) };
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Corner brackets, each an L drawn about the centre and moved in by CSS. */
|
||||||
|
const B = 112, arm = 30;
|
||||||
|
const brackets = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([sx, sy]) =>
|
||||||
|
`M${C + sx * B},${C + sy * (B - arm)} L${C + sx * B},${C + sy * B} L${C + sx * (B - arm)},${C + sy * B}`);
|
||||||
|
---
|
||||||
|
<svg class={`seal seal--${state}`} viewBox="0 0 400 400" role="img" aria-label={label} xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="seal__ticks">
|
||||||
|
{ticks.map((t) => <line x1={t.x1} y1={t.y1} x2={t.x2} y2={t.y2} />)}
|
||||||
|
</g>
|
||||||
|
<g class="seal__rings">
|
||||||
|
<circle cx={C} cy={C} r="150" />
|
||||||
|
<circle class="seal__dashed" cx={C} cy={C} r="122" />
|
||||||
|
<line x1={C} y1="58" x2={C} y2="132" />
|
||||||
|
<line x1={C} y1="268" x2={C} y2="342" />
|
||||||
|
<line x1="58" y1={C} x2="132" y2={C} />
|
||||||
|
<line x1="268" y1={C} x2="342" y2={C} />
|
||||||
|
</g>
|
||||||
|
<g class="seal__brackets">
|
||||||
|
{brackets.map((d) => <path d={d} />)}
|
||||||
|
</g>
|
||||||
|
<circle class="seal__core" cx={C} cy={C} r="52" pathLength="1" />
|
||||||
|
{state === 'confirmed'
|
||||||
|
? <path class="seal__mark" d="M176,201 L194,219 L226,184" pathLength="1" />
|
||||||
|
: <path class="seal__mark" d="M182,182 L218,218 M218,182 L182,218" pathLength="1" />}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.seal {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--ink);
|
||||||
|
/* Drawn small (a few rem), so the strokes are set heavy in the 400-unit box to land near 1px. */
|
||||||
|
stroke-width: 3;
|
||||||
|
stroke-linecap: square;
|
||||||
|
overflow: visible;
|
||||||
|
/* The sequence: lock, then the mark. */
|
||||||
|
--t-lock: 200ms;
|
||||||
|
--t-mark: 700ms;
|
||||||
|
}
|
||||||
|
.seal__ticks { stroke: var(--ink-muted); }
|
||||||
|
.seal__rings { stroke: var(--ink-muted); stroke-opacity: 0.45; }
|
||||||
|
.seal__dashed { stroke-dasharray: 5 11; }
|
||||||
|
|
||||||
|
.seal__brackets path {
|
||||||
|
stroke-width: 6;
|
||||||
|
transform-origin: 200px 200px;
|
||||||
|
animation: seal-lock 520ms var(--ease) both var(--t-lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal__core { stroke-width: 6; }
|
||||||
|
.seal__core, .seal__mark {
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
stroke-dashoffset: 1;
|
||||||
|
animation: seal-draw 600ms var(--ease) forwards var(--t-mark);
|
||||||
|
}
|
||||||
|
.seal__mark { stroke-width: 11; stroke-linecap: butt; animation-delay: calc(var(--t-mark) + 200ms); }
|
||||||
|
.seal--error .seal__core { stroke: var(--ink-muted); stroke-dasharray: 0.02 0.03; }
|
||||||
|
.seal--error .seal__brackets path { animation-name: seal-miss; }
|
||||||
|
|
||||||
|
@keyframes seal-fade { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
@keyframes seal-lock {
|
||||||
|
from { opacity: 0; transform: scale(1.5); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
/* A failed lock: the brackets come in but stop short. */
|
||||||
|
@keyframes seal-miss {
|
||||||
|
from { opacity: 0; transform: scale(1.5); }
|
||||||
|
to { opacity: 0.5; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
@keyframes seal-draw { to { stroke-dashoffset: 0; } }
|
||||||
|
.seal--error .seal__core { animation: seal-fade 600ms var(--ease) both var(--t-mark); stroke-dashoffset: 0; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.seal * { animation: none !important; }
|
||||||
|
.seal__core, .seal__mark { stroke-dashoffset: 0; }
|
||||||
|
.seal--error .seal__brackets path { opacity: 0.5; transform: scale(1.2); }
|
||||||
|
.seal--error .seal__core { stroke-dasharray: 0.02 0.03; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -276,11 +276,23 @@ const en = {
|
|||||||
title: 'Registered.',
|
title: 'Registered.',
|
||||||
body: 'Your address is confirmed. You will hear from us once, when the CipherVerifier alpha is released.',
|
body: 'Your address is confirmed. You will hear from us once, when the CipherVerifier alpha is released.',
|
||||||
link: 'Back to CipherVerifier',
|
link: 'Back to CipherVerifier',
|
||||||
|
seal: 'A target, locked, with a check at its centre.',
|
||||||
|
rows: [
|
||||||
|
{ key: 'Status', value: 'Confirmed' },
|
||||||
|
{ key: 'Signature', value: 'Valid' },
|
||||||
|
{ key: 'List', value: 'CipherVerifier alpha' },
|
||||||
|
{ key: 'Next email', value: 'At release' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: 'Not registered.',
|
title: 'Not registered.',
|
||||||
body: 'The link has expired or is incomplete, or the registration did not go through. Register again, or write to office@ciphercell.de.',
|
body: 'The link has expired or is incomplete, or the registration did not go through. Register again, or write to office@ciphercell.de.',
|
||||||
link: 'Register again',
|
link: 'Register again',
|
||||||
|
seal: 'A target that did not lock, with a cross at its centre.',
|
||||||
|
rows: [
|
||||||
|
{ key: 'Status', value: 'Not confirmed' },
|
||||||
|
{ key: 'Address', value: 'Not stored' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -615,11 +627,23 @@ const de: typeof en = {
|
|||||||
title: 'Registriert.',
|
title: 'Registriert.',
|
||||||
body: 'Ihre Adresse ist bestätigt. Sie hören einmal von uns, sobald die Alpha von CipherVerifier erscheint.',
|
body: 'Ihre Adresse ist bestätigt. Sie hören einmal von uns, sobald die Alpha von CipherVerifier erscheint.',
|
||||||
link: 'Zurück zu CipherVerifier',
|
link: 'Zurück zu CipherVerifier',
|
||||||
|
seal: 'Ein Zielkreuz, eingerastet, mit einem Haken in der Mitte.',
|
||||||
|
rows: [
|
||||||
|
{ key: 'Status', value: 'Bestätigt' },
|
||||||
|
{ key: 'Signatur', value: 'Gültig' },
|
||||||
|
{ key: 'Liste', value: 'CipherVerifier-Alpha' },
|
||||||
|
{ key: 'Nächste E-Mail', value: 'Zur Veröffentlichung' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
title: 'Nicht registriert.',
|
title: 'Nicht registriert.',
|
||||||
body: 'Der Link ist abgelaufen oder unvollständig, oder die Registrierung ist nicht angekommen. Registrieren Sie sich erneut oder schreiben Sie an office@ciphercell.de.',
|
body: 'Der Link ist abgelaufen oder unvollständig, oder die Registrierung ist nicht angekommen. Registrieren Sie sich erneut oder schreiben Sie an office@ciphercell.de.',
|
||||||
link: 'Erneut registrieren',
|
link: 'Erneut registrieren',
|
||||||
|
seal: 'Ein Zielkreuz, nicht eingerastet, mit einem Kreuz in der Mitte.',
|
||||||
|
rows: [
|
||||||
|
{ key: 'Status', value: 'Nicht bestätigt' },
|
||||||
|
{ key: 'Adresse', value: 'Nicht gespeichert' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+64
-11
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
|
import Seal from '../components/verifier/Seal.astro';
|
||||||
import { localePath, t, type Lang } from '../i18n/ui';
|
import { localePath, t, type Lang } from '../i18n/ui';
|
||||||
interface Props { lang: Lang; page: 'confirmed' | 'error' }
|
interface Props { lang: Lang; page: 'confirmed' | 'error' }
|
||||||
const { lang, page } = Astro.props;
|
const { lang, page } = Astro.props;
|
||||||
@@ -8,22 +9,74 @@ const r = p[page];
|
|||||||
const product = localePath(lang, '/cipherverifier');
|
const product = localePath(lang, '/cipherverifier');
|
||||||
---
|
---
|
||||||
<Base lang={lang} title={r.title} description={r.body} noindex>
|
<Base lang={lang} title={r.title} description={r.body} noindex>
|
||||||
<section class="section">
|
<section class="section result" aria-labelledby="result-title">
|
||||||
<div class="container grid">
|
<div class="container result__inner">
|
||||||
<h1 class="display result__title">{r.title}</h1>
|
<div class="result__head">
|
||||||
<div class="result__body">
|
<h1 id="result-title" class="display result__title">{r.title}</h1>
|
||||||
<p class="muted">{r.body}</p>
|
<div class="result__seal">
|
||||||
<a class="link" href={page === 'error' ? `${product}#notify` : product}>{r.link}</a>
|
<Seal state={page} label={r.seal} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="result__text">
|
||||||
|
<p class="result__body">{r.body}</p>
|
||||||
|
<a class="button" href={page === 'error' ? `${product}#notify` : product}>{r.link}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="result__readout hud-frame" aria-label={r.title}>
|
||||||
|
{r.rows.map((row) => (
|
||||||
|
<div class="result__cell">
|
||||||
|
<dt>{row.key}</dt>
|
||||||
|
<dd>{row.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Base>
|
</Base>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.result__title { grid-column: 1 / -1; }
|
.result__inner { display: grid; gap: var(--space-7); }
|
||||||
.result__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); max-width: var(--measure); justify-items: start; }
|
/* The seal sits beside the headline as a mark, about the height of its cap line. */
|
||||||
@media (min-width: 48rem) {
|
/* On narrow screens it stands above the headline; from 64rem it follows it on the same line. */
|
||||||
.result__title { grid-column: 1 / span 8; }
|
.result__head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-5); }
|
||||||
.result__body { grid-column: 1 / span 5; }
|
.result__seal { order: -1; width: 4.5rem; flex: none; }
|
||||||
|
.result__text { display: grid; gap: var(--space-5); justify-items: start; }
|
||||||
|
.result__body { max-width: var(--measure); color: var(--ink-muted); }
|
||||||
|
|
||||||
|
.result__readout {
|
||||||
|
display: grid;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--step-micro);
|
||||||
|
letter-spacing: var(--track-caps);
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: var(--leading-small);
|
||||||
|
}
|
||||||
|
.result__cell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 9rem 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
}
|
||||||
|
.result__cell + .result__cell { border-top: var(--hairline) solid var(--line-base); }
|
||||||
|
.result__readout dt { color: var(--ink-muted); }
|
||||||
|
|
||||||
|
/* The text arrives as the seal locks; the readout last. */
|
||||||
|
.result__text, .result__readout { animation: result-in var(--dur-rise) var(--ease) both; }
|
||||||
|
.result__text { animation-delay: 500ms; }
|
||||||
|
.result__readout { animation-delay: 1000ms; }
|
||||||
|
@keyframes result-in { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.result__text, .result__readout { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 64rem) {
|
||||||
|
.result__inner { grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gap); row-gap: var(--space-7); }
|
||||||
|
.result__head { grid-column: 1 / -1; flex-direction: row; align-items: center; gap: var(--space-6); }
|
||||||
|
.result__seal { order: 0; width: 7.5rem; }
|
||||||
|
.result__text { grid-column: 1 / span 6; }
|
||||||
|
.result__readout { grid-column: 1 / -1; grid-auto-flow: column; grid-auto-columns: 1fr; }
|
||||||
|
.result__cell { grid-template-columns: 1fr; gap: var(--space-1); padding: var(--space-4) var(--space-5); }
|
||||||
|
.result__cell + .result__cell { border-top: 0; border-left: var(--hairline) solid var(--line-base); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user