mirror of
https://github.com/ThisTine/Snip.git
synced 2026-10-18 02:51:38 +07:00
feat(redirect): restyle PIN and 404 pages to match the redesign
Ink palette, no gradients/glow, same card/field/button tokens as the SPA, custom ease-out, press feedback, 'Unlocking…' state on auto-submit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f788ef3588
commit
4a85ae5222
2 files changed
+78
-81
No files matched your search
@@ -8,29 +8,29 @@ func notFoundHTML(shortHost, homeURL string) string {
|
||||
href := html.EscapeString(homeURL)
|
||||
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme:light)">
|
||||
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme:dark)">
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme:light)">
|
||||
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme:dark)">
|
||||
<title>Link not found</title><style>
|
||||
:root{--bg:#fbfbfa;--surface:#fff;--ink:#18181b;--muted:#71717a;--line:#e9e9eb;
|
||||
--grad-from:#2563eb;--grad-to:#7c3aed;--grad-ink:#fff;--glow-1:rgba(37,99,235,.16);--glow-2:rgba(124,58,237,.14)}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#09090b;--surface:#111113;--ink:#fafafa;--muted:#a1a1aa;--line:#232327;
|
||||
--grad-from:#60a5fa;--grad-to:#a78bfa;--grad-ink:#09090b;--glow-1:rgba(96,165,250,.13);--glow-2:rgba(167,139,250,.12)}}
|
||||
body{margin:0;min-height:100vh;display:grid;place-items:center;background-color:var(--bg);color:var(--ink);
|
||||
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;
|
||||
text-align:center;padding:20px;
|
||||
background-image:radial-gradient(60% 45% at 12% 0%,var(--glow-1),transparent 70%),
|
||||
radial-gradient(55% 40% at 92% 100%,var(--glow-2),transparent 70%);
|
||||
background-attachment:fixed;background-repeat:no-repeat}
|
||||
.card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:32px;width:100%;max-width:360px}
|
||||
h1{font-size:44px;font-weight:600;margin:0;letter-spacing:-.03em;
|
||||
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
p{color:var(--muted);font-size:14px;line-height:1.5;margin:8px 0 20px}
|
||||
a{display:block;background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));color:var(--grad-ink);
|
||||
text-decoration:none;font-size:15px;font-weight:500;
|
||||
padding:13px 20px;border-radius:12px;transition:opacity .15s}
|
||||
a:hover{opacity:.9}
|
||||
</style></head><body><div class="card"><h1>404</h1>
|
||||
<p>This short link doesn't exist or was removed.</p>
|
||||
<a href="` + href + `">Go to ` + h + `</a></div></body></html>`
|
||||
:root{--bg:#fafafa;--surface:#fff;--ink:#111113;--muted:#6f6f78;--line:#e7e7ea;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.04),0 2px 8px -2px rgba(0,0,0,.04);--out:cubic-bezier(.23,1,.32,1)}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#0a0a0b;--surface:#121214;--ink:#f4f4f5;--muted:#9b9ba4;--line:#242428;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.4)}}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;background:var(--bg);color:var(--ink);
|
||||
font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;padding:16px}
|
||||
.card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:28px;width:100%;max-width:384px;
|
||||
box-shadow:var(--shadow);animation:rise .28s var(--out) backwards}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
|
||||
.code{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--muted);margin:0 0 8px}
|
||||
h1{font-size:22px;font-weight:600;letter-spacing:-.02em;margin:0}
|
||||
p{color:var(--muted);font-size:14px;line-height:1.5;margin:6px 0 20px}
|
||||
a{display:flex;align-items:center;justify-content:center;height:48px;background:var(--ink);color:var(--bg);
|
||||
text-decoration:none;font-size:15px;font-weight:500;border-radius:12px;transition:opacity .15s ease,transform .16s var(--out)}
|
||||
@media(hover:hover){a:hover{opacity:.85}}
|
||||
a:active{transform:scale(.97)}
|
||||
a:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink)}
|
||||
@media(prefers-reduced-motion:reduce){.card{animation:none}a:active{transform:none}}
|
||||
</style></head><body><div class="card"><p class="code">404</p><h1>Link not found</h1>
|
||||
<p>This short link doesn't exist, or it was removed.</p>
|
||||
<a href="` + href + `">Shorten a link on ` + h + `</a></div></body></html>`
|
||||
}
|
||||
@@ -22,81 +22,76 @@ var pinTmpl = template.Must(template.New("pin").Parse(`<!doctype html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme:light)">
|
||||
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme:dark)">
|
||||
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme:light)">
|
||||
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme:dark)">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Enter PIN · {{.ShortHost}}/{{.Code}}</title>
|
||||
<style>
|
||||
:root{--bg:#fbfbfa;--surface:#fff;--surface-2:#f4f4f5;--ink:#18181b;--muted:#71717a;
|
||||
--line:#e9e9eb;--accent:#2563eb;--accent-soft:rgba(37,99,235,.1);--danger:#dc2626;
|
||||
--grad-from:#2563eb;--grad-to:#7c3aed;--grad-ink:#fff;
|
||||
--glow-1:rgba(37,99,235,.16);--glow-2:rgba(124,58,237,.14)}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#09090b;--surface:#111113;--surface-2:#19191c;--ink:#fafafa;
|
||||
--muted:#a1a1aa;--line:#232327;--accent:#60a5fa;--accent-soft:rgba(96,165,250,.14);--danger:#f87171;
|
||||
--grad-from:#60a5fa;--grad-to:#a78bfa;--grad-ink:#09090b;
|
||||
--glow-1:rgba(96,165,250,.13);--glow-2:rgba(167,139,250,.12)}}
|
||||
:root{--bg:#fafafa;--surface:#fff;--surface-2:#f4f4f5;--ink:#111113;--muted:#6f6f78;--line:#e7e7ea;
|
||||
--ring:#d4d4d8;--soft:rgba(17,17,19,.08);--danger:#dc2626;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.04),0 2px 8px -2px rgba(0,0,0,.04);--out:cubic-bezier(.23,1,.32,1)}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#0a0a0b;--surface:#121214;--surface-2:#1a1a1d;--ink:#f4f4f5;
|
||||
--muted:#9b9ba4;--line:#242428;--ring:#33333a;--soft:rgba(244,244,245,.12);--danger:#f87171;
|
||||
--shadow:0 1px 2px rgba(0,0,0,.4)}}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:20px;
|
||||
background-color:var(--bg);color:var(--ink);
|
||||
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
background-image:radial-gradient(60% 45% at 12% 0%,var(--glow-1),transparent 70%),
|
||||
radial-gradient(55% 40% at 92% 100%,var(--glow-2),transparent 70%);
|
||||
background-attachment:fixed;background-repeat:no-repeat}
|
||||
.card{width:100%;max-width:380px;background:var(--surface);border:1px solid var(--line);
|
||||
border-radius:16px;padding:24px;animation:rise .3s ease-out}
|
||||
body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;padding:16px;
|
||||
background:var(--bg);color:var(--ink);
|
||||
font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
-webkit-font-smoothing:antialiased}
|
||||
.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||
.card{width:100%;max-width:384px;background:var(--surface);border:1px solid var(--line);
|
||||
border-radius:16px;padding:24px;box-shadow:var(--shadow);animation:rise .28s var(--out) backwards}
|
||||
@media(min-width:420px){.card{padding:28px}}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||
.lock{width:40px;height:40px;border-radius:12px;color:var(--grad-ink);
|
||||
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
|
||||
display:grid;place-items:center;margin-bottom:16px}
|
||||
.lock svg{width:20px;height:20px}
|
||||
h1{font-size:20px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
|
||||
h1 span{background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
|
||||
.lock{width:40px;height:40px;border-radius:12px;background:var(--ink);color:var(--bg);
|
||||
display:grid;place-items:center;margin-bottom:20px}
|
||||
.lock svg{width:18px;height:18px}
|
||||
h1{font-size:22px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
|
||||
.sub{margin:0 0 20px;color:var(--muted);font-size:14px;line-height:1.5}
|
||||
.sub b{color:var(--ink);font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
|
||||
.sub b{display:block;margin-top:2px;color:var(--ink);font-weight:600;overflow-wrap:anywhere}
|
||||
.pins{display:flex;gap:6px}
|
||||
@media(min-width:420px){.pins{gap:8px}}
|
||||
.pins input{flex:1;min-width:0;height:52px;padding:0;text-align:center;font-size:20px;font-weight:600;
|
||||
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink);
|
||||
background:var(--surface-2);border:1px solid var(--line);border-radius:12px;outline:none;
|
||||
transition:border-color .15s,box-shadow .15s}
|
||||
.pins input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
||||
.pins input{flex:1;min-width:0;height:48px;padding:0;text-align:center;font-size:18px;font-weight:600;
|
||||
color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:12px;outline:none;
|
||||
transition:border-color .15s ease,box-shadow .15s ease}
|
||||
.pins input:focus{border-color:var(--ring);box-shadow:0 0 0 4px var(--soft)}
|
||||
.err{color:var(--danger);font-size:13px;font-weight:500;margin:10px 0 0;min-height:19px}
|
||||
form.bad .pins input{border-color:var(--danger)}
|
||||
form.bad .pins{animation:shake .4s}
|
||||
@keyframes shake{25%{transform:translateX(-5px)}50%{transform:translateX(5px)}75%{transform:translateX(-3px)}}
|
||||
button{width:100%;height:48px;margin-top:14px;border:0;border-radius:12px;
|
||||
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));color:var(--grad-ink);
|
||||
font-size:15px;font-weight:500;font-family:inherit;
|
||||
cursor:pointer;transition:opacity .15s,transform .1s}
|
||||
button:hover{opacity:.9}
|
||||
button:active{transform:scale(.99)}
|
||||
:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
|
||||
.foot{margin-top:18px;text-align:center;font-size:12px;color:var(--muted)}
|
||||
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||||
form.bad .pins{animation:shake .35s var(--out)}
|
||||
@keyframes shake{25%{transform:translateX(-4px)}50%{transform:translateX(4px)}75%{transform:translateX(-2px)}}
|
||||
button{width:100%;height:48px;margin-top:12px;border:0;border-radius:12px;background:var(--ink);color:var(--bg);
|
||||
font-size:15px;font-weight:500;font-family:inherit;cursor:pointer;
|
||||
transition:opacity .15s ease,transform .16s var(--out)}
|
||||
@media(hover:hover){button:hover{opacity:.85}}
|
||||
button:active{transform:scale(.97)}
|
||||
button:disabled{opacity:.6;cursor:default}
|
||||
:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink)}
|
||||
.foot{margin-top:20px;display:flex;align-items:center;justify-content:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
.mark{width:16px;height:16px;border-radius:5px;background:var(--ink);color:var(--bg);display:grid;place-items:center}
|
||||
.mark svg{width:10px;height:10px}
|
||||
@media(prefers-reduced-motion:reduce){.card{animation-name:fade}form.bad .pins{animation:none}button:active{transform:none}
|
||||
@keyframes fade{from{opacity:0}}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="lock"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></div>
|
||||
<h1>This link is <span>protected</span></h1>
|
||||
<p class="sub">Enter the 6-digit PIN to continue to <b>{{.ShortHost}}/{{.Code}}</b>.</p>
|
||||
<h1>This link is protected</h1>
|
||||
<p class="sub">Enter the 6-digit PIN to open <b class="mono">{{.ShortHost}}/{{.Code}}</b>.</p>
|
||||
<form method="post" action="{{.ActionPath}}" id="f" autocomplete="off"{{if .HasError}} class="bad"{{end}}>
|
||||
<div class="pins" id="pins">
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 1" required>
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 2" required>
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 3" required>
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 4" required>
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 5" required>
|
||||
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 6" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 1" autocomplete="one-time-code" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 2" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 3" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 4" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 5" required>
|
||||
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 6" required>
|
||||
</div>
|
||||
<p class="err" role="alert">{{if .HasError}}That PIN didn't match. Try again.{{end}}</p>
|
||||
<input type="hidden" name="pin" id="pin">
|
||||
<button type="submit">Unlock</button>
|
||||
<button type="submit" id="go">Unlock</button>
|
||||
</form>
|
||||
<div class="foot">{{.ShortHost}}</div>
|
||||
<div class="foot"><span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12"/></svg></span>snip</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
@@ -110,7 +105,7 @@ button:active{transform:scale(.99)}
|
||||
b.value=b.value.replace(/\D/g,'').slice(0,1);
|
||||
if(b.value&&i<boxes.length-1)boxes[i+1].focus();
|
||||
sync();
|
||||
if(hidden.value.length===6)f.submit();
|
||||
if(hidden.value.length===6)go();
|
||||
});
|
||||
b.addEventListener('focus',function(){b.select()});
|
||||
b.addEventListener('keydown',function(e){if(e.key==='Backspace'&&!b.value&&i>0){boxes[i-1].focus();fresh()}});
|
||||
@@ -118,10 +113,12 @@ button:active{transform:scale(.99)}
|
||||
var d=(e.clipboardData.getData('text')||'').replace(/\D/g,'').slice(0,6);
|
||||
if(!d)return;e.preventDefault();fresh();
|
||||
d.split('').forEach(function(c,j){if(boxes[j])boxes[j].value=c});
|
||||
boxes[Math.min(d.length,5)].focus();sync();if(d.length===6)f.submit();
|
||||
boxes[Math.min(d.length,5)].focus();sync();if(d.length===6)go();
|
||||
});
|
||||
});
|
||||
f.addEventListener('submit',sync);
|
||||
// Auto-submit on the 6th digit; the button says what is happening meanwhile.
|
||||
function go(){var b=document.getElementById('go');b.disabled=true;b.textContent='Unlocking…';f.submit()}
|
||||
f.addEventListener('submit',function(){sync();var b=document.getElementById('go');b.disabled=true;b.textContent='Unlocking…'});
|
||||
if(boxes[0])boxes[0].focus();
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user