Dashboard account section, profile API, and CSS account panels
- SSO session auth gating on all protected pages - dashboard.php: account section (profile form + workspace panel), onboarding prompt modal, overview bar extracted to CSS classes, dashboard.css linked in page head - api/profile.php: save/dismiss endpoint for optional profile fields - assets/css/dashboard.css: account grid, dash-account-panel, dash-profile-form, profile-prompt-backdrop modal, overview bar classes, dash-section-kicker, dash-tier-badge base styles - includes/bootstrap.php: dbnToolsMainUserProfile, dbnToolsProfileNeedsPrompt, dbnToolsRequirePageAuth - scripts/sql/004_user_profile_fields.sql: nullable phone, address, and profile_prompt_dismissed_at columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+208
-15
@@ -4,10 +4,7 @@ declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/dashboard.php'));
|
||||
exit;
|
||||
}
|
||||
dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/dashboard.php');
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$tools = dbnToolsLaunchedTools($uiLang);
|
||||
@@ -36,6 +33,17 @@ if ($dashAuthUser !== null) {
|
||||
$dashEmail = strstr($e, '@', true) ?: $e;
|
||||
}
|
||||
|
||||
$dashProfile = $dashIsSso ? dbnToolsMainUserProfile($dashUserId) : null;
|
||||
$dashProfileNeedsPrompt = dbnToolsProfileNeedsPrompt($dashProfile);
|
||||
$dashProfileValue = static function (string $key) use ($dashProfile): string {
|
||||
return (string)($dashProfile[$key] ?? '');
|
||||
};
|
||||
$dashSeatLimit = match ($dashTier) {
|
||||
'pro' => 3,
|
||||
default => 1,
|
||||
};
|
||||
$dashTeamLabel = $dashSeatLimit > 1 ? '1 / ' . $dashSeatLimit . ' seats in use' : 'Single-user workspace';
|
||||
|
||||
// Next refill / billing date
|
||||
$dashNextBilling = '';
|
||||
$dashNextBillingKey = 'next_refill';
|
||||
@@ -218,6 +226,7 @@ $langSuffix = $uiLang !== 'en' ? '?lang=' . urlencode($uiLang) : '';
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
|
||||
<link rel="stylesheet" href="assets/css/dashboard.css">
|
||||
<style>
|
||||
/* ── Dashboard enhancements ───────────────────────────── */
|
||||
details.dash-mcp-details summary::-webkit-details-marker { display: none; }
|
||||
@@ -258,29 +267,29 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||||
?>
|
||||
|
||||
<!-- ── Account overview bar ─────────────────────────────────────── -->
|
||||
<div style="background:#fff; border:1px solid #e5e7eb; border-radius:12px; padding:1.1rem 1.5rem; margin:0 0 1.25rem; display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap;">
|
||||
<div style="display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; min-width:0;">
|
||||
<div class="dash-overview-bar">
|
||||
<div class="dash-overview-bar__left">
|
||||
<span class="dash-tier-badge" style="background:<?= $tierLabel[1] ?>; color:<?= $tierLabel[2] ?>;"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||||
<?php if (!empty($dashDetail['trial_active'])): ?>
|
||||
<span class="dash-tier-badge" style="background:#fef3c7; color:#92400e;"><?= htmlspecialchars(sprintf($dl['trial_badge'], (int)$dashDetail['trial_days_remaining'])) ?></span>
|
||||
<?php endif; ?>
|
||||
<span style="font-size:1.08rem; font-weight:700; color:#00205B; white-space:nowrap;"><?= number_format($eff) ?> <?= $uiLang === 'no' ? 'kred.' : 'credits' ?></span>
|
||||
<span style="color:#9ca3af; font-size:.92rem; white-space:nowrap;"><?= (int)$dashDetail['balance'] ?> <?= $uiLang === 'no' ? 'månedlige' : 'monthly' ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalt' : 'prepaid' ?></span>
|
||||
<span class="dash-overview-bar__credits"><?= number_format($eff) ?> <?= $uiLang === 'no' ? 'kred.' : 'credits' ?></span>
|
||||
<span class="dash-overview-bar__meta"><?= (int)$dashDetail['balance'] ?> <?= $uiLang === 'no' ? 'månedlige' : 'monthly' ?> · <?= (int)$dashDetail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalt' : 'prepaid' ?></span>
|
||||
<?php if ($dashNextBilling): ?>
|
||||
<span style="color:#6b7280; font-size:.92rem; white-space:nowrap;"><?= htmlspecialchars($dl[$dashNextBillingKey]) ?>: <strong style="color:#374151;"><?= htmlspecialchars($dashNextBilling) ?></strong></span>
|
||||
<span class="dash-overview-bar__meta"><?= htmlspecialchars($dl[$dashNextBillingKey]) ?>: <strong><?= htmlspecialchars($dashNextBilling) ?></strong></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($dashEmail): ?>
|
||||
<span style="color:#d1d5db; font-size:.92rem; white-space:nowrap; display:none;" class="dash-email-sep">·</span>
|
||||
<span style="color:#9ca3af; font-size:.92rem; white-space:nowrap;"><?= htmlspecialchars($dl['signed_in_as']) ?>: <strong style="color:#374151;"><?= htmlspecialchars($dashEmail) ?></strong></span>
|
||||
<span class="dash-overview-bar__meta dash-email-sep" style="display:none;">·</span>
|
||||
<span class="dash-overview-bar__meta"><?= htmlspecialchars($dl['signed_in_as']) ?>: <strong><?= htmlspecialchars($dashEmail) ?></strong></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:.5rem; flex-shrink:0; flex-wrap:wrap;">
|
||||
<div class="dash-overview-bar__right">
|
||||
<?php if ($isPaid): ?>
|
||||
<a href="/billing.php" style="color:#6b7280; font-size:.92rem; text-decoration:none; border:1px solid #e5e7eb; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
||||
<a href="/billing.php" class="dash-overview-bar__action dash-overview-bar__action--manage"><?= htmlspecialchars($dl['manage_plan']) ?></a>
|
||||
<?php else: ?>
|
||||
<a href="/pricing.php" style="background:#00205B; color:#fff; font-size:.92rem; font-weight:700; text-decoration:none; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
||||
<a href="/pricing.php" class="dash-overview-bar__action dash-overview-bar__action--upgrade"><?= htmlspecialchars($dl['upgrade_plan']) ?> →</a>
|
||||
<?php endif; ?>
|
||||
<a href="/pricing.php#topup" style="background:#eff6ff; color:#1d4ed8; border:1px solid #bfdbfe; font-size:.92rem; font-weight:600; text-decoration:none; padding:7px 14px; border-radius:7px; white-space:nowrap;"><?= htmlspecialchars($dl['top_up']) ?> →</a>
|
||||
<a href="/pricing.php#topup" class="dash-overview-bar__action dash-overview-bar__action--topup"><?= htmlspecialchars($dl['top_up']) ?> →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -323,6 +332,86 @@ window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section id="account" class="dashboard-account-grid" aria-label="Account and profile">
|
||||
<article class="dash-account-panel dash-account-panel--profile">
|
||||
<div class="dash-account-panel__head">
|
||||
<div>
|
||||
<p class="dash-section-kicker">Account</p>
|
||||
<h2>Profile details</h2>
|
||||
</div>
|
||||
<span class="dash-account-chip">Optional</span>
|
||||
</div>
|
||||
<form id="profileForm" class="dash-profile-form">
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($dashProfileValue('display_name')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Phone</span>
|
||||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($dashProfileValue('phone')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 1</span>
|
||||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($dashProfileValue('address_line1')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 2</span>
|
||||
<input name="address_line2" type="text" maxlength="180" autocomplete="address-line2" value="<?= htmlspecialchars($dashProfileValue('address_line2')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Postal code</span>
|
||||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($dashProfileValue('postal_code')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>City</span>
|
||||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($dashProfileValue('city')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Region</span>
|
||||
<input name="address_region" type="text" maxlength="100" autocomplete="address-level1" value="<?= htmlspecialchars($dashProfileValue('address_region')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Country</span>
|
||||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($dashProfileValue('country')) ?>">
|
||||
</label>
|
||||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
||||
<div class="dash-profile-actions span-2">
|
||||
<button type="submit" class="dash-profile-save">Save profile</button>
|
||||
<p id="profileStatus" class="dash-profile-status" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="dash-account-panel">
|
||||
<div class="dash-account-panel__head">
|
||||
<div>
|
||||
<p class="dash-section-kicker">Users</p>
|
||||
<h2>Workspace access</h2>
|
||||
</div>
|
||||
<span class="dash-account-chip"><?= htmlspecialchars($tierLabel[0]) ?></span>
|
||||
</div>
|
||||
<div class="dash-account-list">
|
||||
<div>
|
||||
<span>Email</span>
|
||||
<strong><?= htmlspecialchars((string)($dashAuthUser['email'] ?? '')) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Seats</span>
|
||||
<strong><?= htmlspecialchars($dashTeamLabel) ?></strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Role</span>
|
||||
<strong>Owner</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Plan storage</span>
|
||||
<strong><?= $isPaid ? htmlspecialchars(round((int)$dashDetail['storage_quota_bytes'] / 1048576) . ' MB') : 'Upgrade for My Case storage' ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dash-account-note">Team invitations and seat management will be added after this consolidation pass.</p>
|
||||
</article>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="disclaimer" role="note"><?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?></div>
|
||||
@@ -500,6 +589,47 @@ foreach ($tools as $slug => $item):
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($dashIsSso && $dashProfileNeedsPrompt): ?>
|
||||
<div id="profilePromptModal" class="profile-prompt-backdrop" role="dialog" aria-modal="true" aria-labelledby="profilePromptTitle">
|
||||
<div class="profile-prompt-card">
|
||||
<p class="dash-section-kicker">Optional profile</p>
|
||||
<h2 id="profilePromptTitle">Add contact details for your tools account</h2>
|
||||
<p>These details help support and billing conversations. They are optional and never block tool access.</p>
|
||||
<form id="profilePromptForm" class="dash-profile-form">
|
||||
<label>
|
||||
<span>Display name</span>
|
||||
<input name="display_name" type="text" maxlength="100" autocomplete="name" value="<?= htmlspecialchars($dashProfileValue('display_name')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Phone</span>
|
||||
<input name="phone" type="tel" maxlength="40" autocomplete="tel" value="<?= htmlspecialchars($dashProfileValue('phone')) ?>">
|
||||
</label>
|
||||
<label class="span-2">
|
||||
<span>Address line 1</span>
|
||||
<input name="address_line1" type="text" maxlength="180" autocomplete="address-line1" value="<?= htmlspecialchars($dashProfileValue('address_line1')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Postal code</span>
|
||||
<input name="postal_code" type="text" maxlength="32" autocomplete="postal-code" value="<?= htmlspecialchars($dashProfileValue('postal_code')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>City</span>
|
||||
<input name="city" type="text" maxlength="100" autocomplete="address-level2" value="<?= htmlspecialchars($dashProfileValue('city')) ?>">
|
||||
</label>
|
||||
<label>
|
||||
<span>Country</span>
|
||||
<input name="country" type="text" maxlength="2" autocomplete="country" placeholder="NO" value="<?= htmlspecialchars($dashProfileValue('country')) ?>">
|
||||
</label>
|
||||
<input type="hidden" name="preferred_language" value="<?= htmlspecialchars($uiLang) ?>">
|
||||
<div class="profile-prompt-actions span-2">
|
||||
<button type="button" id="profilePromptSkip">Skip for now</button>
|
||||
<button type="submit">Save details</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</main>
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
<script src="assets/js/tools.js" defer></script>
|
||||
@@ -530,6 +660,69 @@ foreach ($tools as $slug => $item):
|
||||
}
|
||||
}());
|
||||
|
||||
/* Profile details */
|
||||
(function () {
|
||||
function payloadFromForm(form, dismiss) {
|
||||
var data = {};
|
||||
if (form) {
|
||||
Array.prototype.forEach.call(form.elements, function (el) {
|
||||
if (!el.name) return;
|
||||
data[el.name] = el.value || '';
|
||||
});
|
||||
}
|
||||
if (dismiss) data.dismiss_prompt = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
function saveProfile(payload) {
|
||||
return fetch('/api/profile.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function (r) {
|
||||
return r.json().then(function (data) {
|
||||
if (!r.ok || !data.ok) {
|
||||
throw new Error(data.error && data.error.message ? data.error.message : 'Could not save profile');
|
||||
}
|
||||
return data;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var profileForm = document.getElementById('profileForm');
|
||||
var profileStatus = document.getElementById('profileStatus');
|
||||
if (profileForm) {
|
||||
profileForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (profileStatus) profileStatus.textContent = 'Saving...';
|
||||
saveProfile(payloadFromForm(profileForm, true))
|
||||
.then(function () { if (profileStatus) profileStatus.textContent = 'Saved'; })
|
||||
.catch(function (err) { if (profileStatus) profileStatus.textContent = err.message; });
|
||||
});
|
||||
}
|
||||
|
||||
var promptModal = document.getElementById('profilePromptModal');
|
||||
var promptForm = document.getElementById('profilePromptForm');
|
||||
var promptSkip = document.getElementById('profilePromptSkip');
|
||||
function closePrompt() {
|
||||
if (promptModal) promptModal.hidden = true;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
if (promptModal) document.body.style.overflow = 'hidden';
|
||||
if (promptForm) {
|
||||
promptForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
saveProfile(payloadFromForm(promptForm, true)).then(closePrompt).catch(function (err) { alert(err.message); });
|
||||
});
|
||||
}
|
||||
if (promptSkip) {
|
||||
promptSkip.addEventListener('click', function () {
|
||||
saveProfile(payloadFromForm(promptForm, true)).then(closePrompt).catch(closePrompt);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
||||
/* ── Corpus summary ─────────────────────────────────────────────────── */
|
||||
<?php if ($dashIsSso): ?>
|
||||
(function () {
|
||||
|
||||
Reference in New Issue
Block a user