Files
pictureFrame-webApp/templates/setup/index.html.twig
T
football2801 a302ac09b4
CI / test (push) Has been cancelled
feat(design): v2 opt-in (atmospheric dusks) — Settings toggle, cookie-mirrored
Lets users opt into the new atmospheric design without affecting users on v1.
Adds a beta-flag toggle in Settings → Design. Server-side preference persists
across devices; a cookie mirrors it so unauthenticated Twig pages do correct
first-paint without an extra DB roundtrip.

Backend:
- User.designVersion column (nullable VARCHAR(10); null defaults to 'v1')
- Migration Version20260515120000
- PATCH /api/user/design endpoint accepting 'v1'|'v2', sets wevisto_design cookie
- SpaController injects data-design on <html> + refreshes the cookie on every
  SPA load (keeps cross-device pref in sync)
- Twig templates (base, login, register, help, setup, token-*) read the
  cookie via {{ app.request.cookies.get('wevisto_design')|default('v1') }}
  so login/setup pages also respect the user's design choice

Frontend:
- design-v2.scss — opt-in overlay scoped under [data-design="v2"]. Overrides
  --color-* tokens to dusk variants per theme (warm-craft → amber, ocean-dusk
  stays, etc.), adds harbor photo backdrop via body::before with theme tint
  via body::after. Glass-card blur on existing surfaces. v1 untouched.
- harbor.jpg shipped as a public asset (270KB, single-fetch, cached)
- User type gains designVersion ('v1' | 'v2')
- SettingsView toggle (Original / Atmospheric) calls the API, updates the
  data-design attribute optimistically, reverts on failure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:28:44 -04:00

154 lines
8.6 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-design="{{ app.request.cookies.get('wevisto_design')|default('v1') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/build/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/build/icons/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/build/icons/favicon-16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/build/icons/apple-touch-icon.png">
<title>Set up your frame — WeVisto</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; min-height: 100dvh; background: #fdf6ee; color: #3a2e22;
display: flex; align-items: flex-start; justify-content: center; padding: 2rem 1rem; }
.card { width: 100%; max-width: 400px; }
h1 { font-size: 1.4rem; font-weight: 700; margin-bottom: .25rem; }
.subtitle { font-size: .875rem; color: #8a7060; margin-bottom: 1.5rem; }
.tabs { display: flex; border-bottom: 1px solid #e8d9c4; margin-bottom: 1.5rem; }
.tab { flex: 1; padding: .75rem; text-align: center; font-weight: 700; font-size: .9rem;
color: #8a7060; text-decoration: none; border-bottom: 2px solid transparent; transition: color .15s; }
.tab.active { color: #c97c3a; border-bottom-color: #c97c3a; }
.panel { display: none; } .panel.active { display: block; }
.field { margin-bottom: 1rem; }
label { display: block; font-size: .8125rem; font-weight: 600; color: #8a7060; margin-bottom: .375rem; }
input[type="email"], input[type="password"], input[type="text"] {
width: 100%; min-height: 44px; padding: 0 .875rem; border: 1px solid #e8d9c4;
border-radius: 10px; background: #fff; font-size: 1rem; color: #3a2e22; }
input[aria-invalid="true"] { border-color: #c0392b; }
.field-error { margin-top: .25rem; font-size: .8125rem; color: #c0392b; }
.btn { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 44px;
margin-top: 1.25rem; background: #c97c3a; color: #fff; border: none; border-radius: 9999px;
font-size: 1rem; font-weight: 700; cursor: pointer; }
.claim-banner { background: #fff5e8; border: 1px solid #f0c987; border-radius: 10px;
padding: .75rem .875rem; margin-bottom: 1.25rem; font-size: .875rem; line-height: 1.4;
color: #5c3f1c; }
.claim-banner strong { display: block; margin-bottom: .25rem; }
.claim-check { display: flex; align-items: flex-start; gap: .625rem; margin-top: 1rem;
font-size: .875rem; line-height: 1.35; cursor: pointer; }
.claim-check input[type="checkbox"] { width: 18px; height: 18px; flex: 0 0 auto;
margin-top: 2px; accent-color: #c97c3a; cursor: pointer; }
.logo-badge { display: block; width: 80px; height: 80px; margin: 0 auto 1rem; border-radius: 14px; overflow: hidden; border: 1px solid #e8d9c4; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
.logo-badge img { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<div class="card">
<a href="/" class="logo-badge" aria-label="WeVisto"><img src="/build/logo.svg" alt=""></a>
<h1>Link this frame</h1>
<p class="subtitle">Create an account, or sign in if you already have one. The frame will link to whichever account you use here.</p>
{% if already_claimed %}
<p class="claim-banner" role="status">
<strong>This frame is already linked to another account.</strong>
If youre taking it over, tick the box below — the previous
owners photos and history for this frame will be permanently
removed.
</p>
{% if claim_error %}
<p class="field-error" role="alert" style="margin-bottom:1rem">{{ claim_error }}</p>
{% endif %}
{% endif %}
<div class="tabs">
<a href="#register" class="tab {% if not login_error %}active{% endif %}" data-tab="register">Create account</a>
<a href="#login" class="tab {% if login_error %}active{% endif %}" data-tab="login">Sign in</a>
</div>
{# ── Register panel ────────────────────────────────────────────────────── #}
<div id="register" class="panel {% if not login_error %}active{% endif %}">
{{ form_start(reg_form, {action: path('setup_register', {mac: mac}), attr: {novalidate: 'novalidate'}}) }}
<div class="field">
{{ form_label(reg_form.email) }}
{{ form_widget(reg_form.email, {attr: {
id: 'reg-email',
'aria-invalid': reg_form.email.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
{% for error in reg_form.email.vars.errors %}
<p class="field-error" role="alert">{{ error.message }}</p>
{% endfor %}
</div>
<div class="field">
{{ form_label(reg_form.plainPassword.first) }}
{{ form_widget(reg_form.plainPassword.first, {attr: {
id: 'reg-pass',
autocomplete: 'new-password',
'aria-invalid': reg_form.plainPassword.first.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
{% for error in reg_form.plainPassword.first.vars.errors %}
<p class="field-error" role="alert">{{ error.message }}</p>
{% endfor %}
</div>
<div class="field">
{{ form_label(reg_form.plainPassword.second) }}
{{ form_widget(reg_form.plainPassword.second, {attr: {
id: 'reg-pass-confirm',
autocomplete: 'new-password',
'aria-invalid': reg_form.plainPassword.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
{% for error in reg_form.plainPassword.vars.errors %}
<p class="field-error" role="alert">{{ error.message }}</p>
{% endfor %}
</div>
{% if already_claimed %}
<label class="claim-check">
<input type="checkbox" name="claim_device" value="1" required>
<span>Claim this frame as my own (deletes the previous owners photos and history)</span>
</label>
{% endif %}
<button type="submit" class="btn">Create account &amp; link frame</button>
{{ form_end(reg_form) }}
</div>
{# ── Login panel ───────────────────────────────────────────────────────── #}
<div id="login" class="panel {% if login_error %}active{% endif %}">
<form method="post" action="{{ path('setup_login', {mac: mac}) }}" novalidate>
{% if login_error %}
<p class="field-error" role="alert" style="margin-bottom:1rem">{{ login_error }}</p>
{% endif %}
<div class="field">
<label for="login-email">Email address</label>
<input type="email" id="login-email" name="_username" autocomplete="email" min-height="44px">
</div>
<div class="field">
<label for="login-pass">Password</label>
<input type="password" id="login-pass" name="_password" autocomplete="current-password">
</div>
{% if already_claimed %}
<label class="claim-check">
<input type="checkbox" name="claim_device" value="1" required>
<span>Claim this frame as my own (deletes the previous owners photos and history)</span>
</label>
{% endif %}
<button type="submit" class="btn">Sign in &amp; link frame</button>
</form>
</div>
</div>
<script>
(function () {
var tabs = document.querySelectorAll('.tab');
var panels = document.querySelectorAll('.panel');
tabs.forEach(function (tab) {
tab.addEventListener('click', function (e) {
e.preventDefault();
var target = tab.dataset.tab;
tabs.forEach(function (t) { t.classList.toggle('active', t.dataset.tab === target); });
panels.forEach(function (p) { p.classList.toggle('active', p.id === target); });
});
});
}());
</script>
</body>
</html>