ui: add firefly card and onboarding guides
This commit is contained in:
parent
341e10db3d
commit
f08ec8310e
@ -21,11 +21,78 @@
|
|||||||
Change password
|
Change password
|
||||||
</a>
|
</a>
|
||||||
<button v-else class="pill mono" type="button" @click="doLogin">Login</button>
|
<button v-else class="pill mono" type="button" @click="doLogin">Login</button>
|
||||||
|
<a class="pill mono" href="/onboarding">Onboarding</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="auth.ready && auth.authenticated">
|
<section v-if="auth.ready && auth.authenticated">
|
||||||
<div class="account-grid">
|
<div class="account-grid">
|
||||||
|
<div class="account-column">
|
||||||
|
<div class="card module">
|
||||||
|
<div class="module-head">
|
||||||
|
<h2>Firefly III</h2>
|
||||||
|
<span
|
||||||
|
class="pill mono"
|
||||||
|
:class="
|
||||||
|
firefly.status === 'ready'
|
||||||
|
? 'pill-ok'
|
||||||
|
: firefly.status === 'needs provisioning' || firefly.status === 'login required'
|
||||||
|
? 'pill-warn'
|
||||||
|
: firefly.status === 'unavailable' || firefly.status === 'error'
|
||||||
|
? 'pill-bad'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ firefly.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted">
|
||||||
|
Personal finance manager for budgets and spending. Sign in with the email below and set the server URL to
|
||||||
|
money.bstein.dev in the Abacus mobile app.
|
||||||
|
</p>
|
||||||
|
<div class="kv">
|
||||||
|
<div class="row">
|
||||||
|
<span class="k mono">URL</span>
|
||||||
|
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noreferrer">
|
||||||
|
money.bstein.dev
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="k mono">Email</span>
|
||||||
|
<span class="v mono">{{ firefly.username || auth.email || auth.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="k mono">Password updated</span>
|
||||||
|
<span class="v mono">{{ firefly.passwordUpdatedAt || "unknown" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="primary" type="button" :disabled="firefly.resetting" @click="resetFirefly">
|
||||||
|
{{ firefly.resetting ? "Resetting..." : "Reset Firefly password" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="firefly.password" class="secret-box">
|
||||||
|
<div class="secret-head">
|
||||||
|
<div class="pill mono">Password</div>
|
||||||
|
<div class="secret-actions">
|
||||||
|
<button class="copy mono" type="button" @click="copy('firefly-password', firefly.password)">
|
||||||
|
copy
|
||||||
|
<span v-if="copied['firefly-password']" class="copied">copied</span>
|
||||||
|
</button>
|
||||||
|
<button class="copy mono" type="button" @click="firefly.revealPassword = !firefly.revealPassword">
|
||||||
|
{{ firefly.revealPassword ? "hide" : "show" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mono secret">{{ firefly.revealPassword ? firefly.password : "••••••••••••••••" }}</div>
|
||||||
|
<div class="hint mono">Use this in Firefly III and the Abacus app.</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
||||||
|
<div v-if="firefly.error" class="error-box">
|
||||||
|
<div class="mono">{{ firefly.error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card module">
|
<div class="card module">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
<h2>Mail</h2>
|
<h2>Mail</h2>
|
||||||
@ -122,6 +189,7 @@
|
|||||||
<div class="mono">{{ nextcloudMail.error }}</div>
|
<div class="mono">{{ nextcloudMail.error }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="account-stack">
|
<div class="account-stack">
|
||||||
<div class="card module">
|
<div class="card module">
|
||||||
@ -140,10 +208,7 @@
|
|||||||
{{ vaultwarden.status }}
|
{{ vaultwarden.status }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p v-if="vaultwarden.status !== 'ready' && vaultwarden.status !== 'already_present'" class="muted">
|
||||||
v-if="vaultwarden.status !== 'ready' && vaultwarden.status !== 'already_present'"
|
|
||||||
class="muted"
|
|
||||||
>
|
|
||||||
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
|
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
|
||||||
</p>
|
</p>
|
||||||
<div class="kv">
|
<div class="kv">
|
||||||
@ -193,7 +258,9 @@
|
|||||||
<div class="kv">
|
<div class="kv">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="k mono">URL</span>
|
<span class="k mono">URL</span>
|
||||||
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noreferrer">health.bstein.dev</a>
|
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noreferrer">
|
||||||
|
health.bstein.dev
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="k mono">Username</span>
|
<span class="k mono">Username</span>
|
||||||
@ -231,69 +298,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card module">
|
|
||||||
<div class="module-head">
|
|
||||||
<h2>Firefly III</h2>
|
|
||||||
<span
|
|
||||||
class="pill mono"
|
|
||||||
:class="
|
|
||||||
firefly.status === 'ready'
|
|
||||||
? 'pill-ok'
|
|
||||||
: firefly.status === 'needs provisioning' || firefly.status === 'login required'
|
|
||||||
? 'pill-warn'
|
|
||||||
: firefly.status === 'unavailable' || firefly.status === 'error'
|
|
||||||
? 'pill-bad'
|
|
||||||
: ''
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ firefly.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="muted">
|
|
||||||
Personal finance manager for budgets and spending. Sign in with the email below and set the server URL to
|
|
||||||
money.bstein.dev in the Abacus mobile app.
|
|
||||||
</p>
|
|
||||||
<div class="kv">
|
|
||||||
<div class="row">
|
|
||||||
<span class="k mono">URL</span>
|
|
||||||
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noreferrer">money.bstein.dev</a>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<span class="k mono">Email</span>
|
|
||||||
<span class="v mono">{{ firefly.username || auth.email || auth.username }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<span class="k mono">Password updated</span>
|
|
||||||
<span class="v mono">{{ firefly.passwordUpdatedAt || "unknown" }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button class="primary" type="button" :disabled="firefly.resetting" @click="resetFirefly">
|
|
||||||
{{ firefly.resetting ? "Resetting..." : "Reset Firefly password" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="firefly.password" class="secret-box">
|
|
||||||
<div class="secret-head">
|
|
||||||
<div class="pill mono">Password</div>
|
|
||||||
<div class="secret-actions">
|
|
||||||
<button class="copy mono" type="button" @click="copy('firefly-password', firefly.password)">
|
|
||||||
copy
|
|
||||||
<span v-if="copied['firefly-password']" class="copied">copied</span>
|
|
||||||
</button>
|
|
||||||
<button class="copy mono" type="button" @click="firefly.revealPassword = !firefly.revealPassword">
|
|
||||||
{{ firefly.revealPassword ? "hide" : "show" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mono secret">{{ firefly.revealPassword ? firefly.password : "••••••••••••••••" }}</div>
|
|
||||||
<div class="hint mono">Use this in Firefly III and the Abacus app.</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
|
||||||
<div v-if="firefly.error" class="error-box">
|
|
||||||
<div class="mono">{{ firefly.error }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card module">
|
<div class="card module">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
<h2>Jellyfin</h2>
|
<h2>Jellyfin</h2>
|
||||||
@ -317,7 +321,9 @@
|
|||||||
<div class="kv">
|
<div class="kv">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="k mono">URL</span>
|
<span class="k mono">URL</span>
|
||||||
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noreferrer">stream.bstein.dev</a>
|
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noreferrer">
|
||||||
|
stream.bstein.dev
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="k mono">Username</span>
|
<span class="k mono">Username</span>
|
||||||
@ -789,19 +795,20 @@ h1 {
|
|||||||
|
|
||||||
.account-grid {
|
.account-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-column,
|
||||||
.account-stack {
|
.account-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
height: 100%;
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-column .module,
|
||||||
.account-stack .module {
|
.account-stack .module {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -944,6 +951,10 @@ button.primary {
|
|||||||
.account-stack .module {
|
.account-stack .module {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-column .module {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin {
|
.admin {
|
||||||
|
|||||||
@ -451,6 +451,49 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="mobile-guides">
|
||||||
|
<div class="module-head">
|
||||||
|
<h3>Mobile app guides</h3>
|
||||||
|
<span class="pill mono">step-by-step</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted">
|
||||||
|
Each guide expands with screenshots. Set the server URL before logging in so everything points at Atlas.
|
||||||
|
</p>
|
||||||
|
<div class="guide-grid">
|
||||||
|
<div v-for="guide in mobileGuides" :key="guide.id" class="card module guide-card">
|
||||||
|
<div class="module-head">
|
||||||
|
<h3>{{ guide.title }}</h3>
|
||||||
|
<span class="pill mono">{{ guide.app }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted">{{ guide.description }}</p>
|
||||||
|
<div class="kv">
|
||||||
|
<div v-for="field in guide.fields" :key="`${guide.id}-${field.label}`" class="row">
|
||||||
|
<span class="k mono">{{ field.label }}</span>
|
||||||
|
<span class="v mono">
|
||||||
|
<a v-if="field.href" class="guide-link" :href="field.href" target="_blank" rel="noreferrer">
|
||||||
|
{{ field.value }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ field.value }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="guide-actions">
|
||||||
|
<a class="pill mono guide-link" :href="guide.url" target="_blank" rel="noreferrer">Open</a>
|
||||||
|
</div>
|
||||||
|
<details class="guide-details">
|
||||||
|
<summary class="mono">Photo guide</summary>
|
||||||
|
<div v-if="guideShots[guide.id] && guideShots[guide.id].length" class="guide-images">
|
||||||
|
<figure v-for="(shot, index) in guideShots[guide.id]" :key="shot.url" class="guide-shot">
|
||||||
|
<img :src="shot.url" :alt="`${guide.title} step ${index + 1}`" loading="lazy" />
|
||||||
|
<figcaption v-if="shot.label" class="mono">{{ shot.label }}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p v-else class="muted">Guide coming soon.</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="status === 'ready'" class="ready-box">
|
<div v-if="status === 'ready'" class="ready-box">
|
||||||
<h3>You're ready</h3>
|
<h3>You're ready</h3>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
@ -537,6 +580,112 @@ const extraSteps = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Guide images: drop files into src/assets/onboarding/<guideId>/01-step.png to auto-load and order.
|
||||||
|
const guideShots = buildGuideShots(
|
||||||
|
import.meta.glob("../assets/onboarding/**/*.{png,jpg,jpeg,webp}", { eager: true, as: "url" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobileGuides = [
|
||||||
|
{
|
||||||
|
id: "vaultwarden",
|
||||||
|
title: "Vaultwarden",
|
||||||
|
app: "Bitwarden",
|
||||||
|
description: "Password manager with autofill across devices.",
|
||||||
|
url: "https://vault.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Server", value: "vault.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Atlas email + password" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "elementx",
|
||||||
|
title: "Element X",
|
||||||
|
app: "Matrix chat",
|
||||||
|
description: "Secure chat for Othrys rooms and direct messages.",
|
||||||
|
url: "https://live.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Homeserver", value: "live.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Atlas username + password" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wger",
|
||||||
|
title: "Wger",
|
||||||
|
app: "Health tracking",
|
||||||
|
description: "Workout + nutrition tracking for Atlas.",
|
||||||
|
url: "https://health.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Server", value: "health.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Use Account page credentials" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "firefly",
|
||||||
|
title: "Firefly III",
|
||||||
|
app: "Abacus",
|
||||||
|
description: "Personal finance manager for budgets and spending.",
|
||||||
|
url: "https://money.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Server", value: "money.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Use Account page credentials" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nextcloud",
|
||||||
|
title: "Nextcloud",
|
||||||
|
app: "Files + mail",
|
||||||
|
description: "Files, calendar, and mail on the go.",
|
||||||
|
url: "https://cloud.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Server", value: "cloud.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Atlas username + password" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "jellyfin",
|
||||||
|
title: "Jellyfin",
|
||||||
|
app: "Media streaming",
|
||||||
|
description: "Personal media library for Atlas.",
|
||||||
|
url: "https://stream.bstein.dev",
|
||||||
|
fields: [
|
||||||
|
{ label: "Server", value: "stream.bstein.dev" },
|
||||||
|
{ label: "Login", value: "Atlas username + password" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildGuideShots(rawShots) {
|
||||||
|
const grouped = {};
|
||||||
|
for (const [path, url] of Object.entries(rawShots)) {
|
||||||
|
const normalized = path.replace(/\\/g, "/");
|
||||||
|
const parts = normalized.split("/assets/onboarding/");
|
||||||
|
if (parts.length < 2) continue;
|
||||||
|
const [guideId, file] = parts[1].split("/");
|
||||||
|
if (!guideId || !file) continue;
|
||||||
|
const order = guideOrder(file);
|
||||||
|
const label = guideLabel(file);
|
||||||
|
if (!grouped[guideId]) grouped[guideId] = [];
|
||||||
|
grouped[guideId].push({ url, order, label, file });
|
||||||
|
}
|
||||||
|
Object.values(grouped).forEach((shots) => {
|
||||||
|
shots.sort((a, b) => (a.order - b.order) || a.file.localeCompare(b.file));
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guideOrder(filename) {
|
||||||
|
const prefix = filename.match(/^(\d{1,3})/);
|
||||||
|
if (prefix) return Number(prefix[1]);
|
||||||
|
const step = filename.match(/step[-_]?(\d{1,3})/i);
|
||||||
|
if (step) return Number(step[1]);
|
||||||
|
return Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guideLabel(filename) {
|
||||||
|
const base = filename.replace(/\.(png|jpe?g|webp)$/i, "");
|
||||||
|
return base.replace(/^\d+[-_]?/, "").replace(/[-_]/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
function statusLabel(value) {
|
function statusLabel(value) {
|
||||||
const key = (value || "").trim();
|
const key = (value || "").trim();
|
||||||
if (key === "pending_email_verification") return "confirm email";
|
if (key === "pending_email_verification") return "confirm email";
|
||||||
@ -1228,6 +1377,73 @@ button.secondary {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-guides {
|
||||||
|
margin-top: 18px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-link {
|
||||||
|
color: rgba(125, 208, 255, 0.9);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-details {
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(0, 0, 0, 0.16);
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-images {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-shot {
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-shot img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-shot figcaption {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.recovery-verify {
|
.recovery-verify {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user