Onboarding / Deployment Scripts / Marketing updates
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
|
||||
export let context: 'onboarding' | 'contract' = 'onboarding';
|
||||
|
||||
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, string>; draft: Record<string, unknown> } }>();
|
||||
|
||||
const ownerEmail = 'info@goodwalk.co.nz';
|
||||
const ownerPhone = '(022) 642 1011';
|
||||
|
||||
let stage: 'email' | 'code' = 'email';
|
||||
let emailValue = '';
|
||||
let codeValue = '';
|
||||
let loading = false;
|
||||
let error = '';
|
||||
|
||||
async function requestCode() {
|
||||
const trimmed = emailValue.trim();
|
||||
if (!trimmed) { error = 'Please enter your email address'; return; }
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch('/api/auth/request-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: trimmed }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.detail ?? 'Failed to send code. Please try again.');
|
||||
stage = 'code';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Something went wrong';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCode() {
|
||||
const trimmed = codeValue.trim();
|
||||
if (!trimmed) { error = 'Please enter the code'; return; }
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch('/api/auth/verify-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailValue.trim(), code: trimmed }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.detail ?? 'Incorrect code. Please try again.');
|
||||
try { window.localStorage.setItem('gw_onboarding_session', data.token); } catch { /* ignore */ }
|
||||
let profile: Record<string, string> = {};
|
||||
let draft: Record<string, unknown> = {};
|
||||
try {
|
||||
const verifyRes = await fetch('/api/auth/verify', {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
if (verifyRes.ok) {
|
||||
const verifyData = await verifyRes.json();
|
||||
profile = verifyData.profile ?? {};
|
||||
draft = verifyData.draft ?? {};
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
dispatch('authenticated', { email: data.email, profile, draft });
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Something went wrong';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEmailKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') requestCode();
|
||||
}
|
||||
|
||||
function handleCodeKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') verifyCode();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
stage = 'email';
|
||||
codeValue = '';
|
||||
error = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<div class="auth-icon">
|
||||
<Icon name="fas fa-lock" />
|
||||
</div>
|
||||
|
||||
{#if stage === 'email'}
|
||||
<h2>Sign in to continue</h2>
|
||||
<p>Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code.</p>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-email">Email address</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
bind:value={emailValue}
|
||||
on:keydown={handleEmailKey}
|
||||
placeholder="you@example.com"
|
||||
autocomplete="email"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="auth-error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-yellow auth-btn" on:click={requestCode} disabled={loading}>
|
||||
{#if loading}Sending…{:else}Send code <Icon name="fas fa-arrow-right" />{/if}
|
||||
</button>
|
||||
|
||||
{:else}
|
||||
<h2>Enter your code</h2>
|
||||
<p>We sent a 6-digit code to <strong>{emailValue}</strong>. It expires in 10 minutes.</p>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-code">One-time code</label>
|
||||
<input
|
||||
id="auth-code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="6"
|
||||
bind:value={codeValue}
|
||||
on:keydown={handleCodeKey}
|
||||
placeholder="123456"
|
||||
autocomplete="one-time-code"
|
||||
disabled={loading}
|
||||
class="auth-code-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="auth-error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-yellow auth-btn" on:click={verifyCode} disabled={loading}>
|
||||
{#if loading}Verifying…{:else}Verify code <Icon name="fas fa-arrow-right" />{/if}
|
||||
</button>
|
||||
|
||||
<button class="auth-back" on:click={goBack}>
|
||||
<Icon name="fas fa-arrow-left" /> Use a different email
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="auth-help">
|
||||
<span>Need help?</span>
|
||||
<a href="mailto:{ownerEmail}">{ownerEmail}</a>
|
||||
<span>or</span>
|
||||
<a href="tel:{ownerPhone.replace(/[^0-9+]/g, '')}">{ownerPhone}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="auth-copyright">
|
||||
<a href="https://goodwalk.co.nz">goodwalk.co.nz</a>
|
||||
<span>·</span>
|
||||
<span>© {new Date().getFullYear()} Goodwalk. All rights reserved.</span>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.auth-wrap {
|
||||
padding: 32px 28px 64px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 36px 32px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(33, 48, 33, 0.08);
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.09);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffe36b 0%, #ffd100 100%);
|
||||
color: #213021;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.auth-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(22px, 3vw, 30px);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-card p {
|
||||
margin: 0 0 24px;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
color: rgba(33, 48, 33, 0.72);
|
||||
}
|
||||
|
||||
.auth-card p strong {
|
||||
color: #213021;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-field label {
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-field input {
|
||||
width: 100%;
|
||||
padding: 15px 16px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.14);
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
color: #213021;
|
||||
outline: none;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-field input:focus {
|
||||
border-color: rgba(255, 209, 0, 0.9);
|
||||
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.16);
|
||||
}
|
||||
|
||||
.auth-code-input {
|
||||
font-size: 28px !important;
|
||||
font-family: var(--font-head) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: 0.22em !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff3ef;
|
||||
color: #a43f2c;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.12);
|
||||
background: transparent;
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(33, 48, 33, 0.65);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin-bottom: 20px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.auth-back:hover {
|
||||
background: rgba(33, 48, 33, 0.05);
|
||||
}
|
||||
|
||||
.auth-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(33, 48, 33, 0.07);
|
||||
font-size: 13px;
|
||||
color: rgba(33, 48, 33, 0.5);
|
||||
}
|
||||
|
||||
.auth-help a {
|
||||
color: rgba(33, 48, 33, 0.75);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-help a:hover {
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-copyright {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.07);
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-copyright a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-copyright a:hover {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-wrap {
|
||||
padding: 20px 18px 32px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 26px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user