diff --git a/account.php b/account.php
index 5e775b8..3190e2a 100644
--- a/account.php
+++ b/account.php
@@ -5,10 +5,10 @@ require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/PricingCatalog.php';
-if (!dbnToolsIsAuthenticated()) {
- header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/account.php'));
- exit;
-}
+dbnToolsRequirePageAuth('/dashboard.php#account');
+
+header('Location: /dashboard.php#account');
+exit;
$uiLang = dbnToolsCurrentLanguage();
$authUser = dbnToolsAuthenticatedUser();
diff --git a/api/logout.php b/api/logout.php
index 5bbc230..28cf364 100644
--- a/api/logout.php
+++ b/api/logout.php
@@ -15,5 +15,6 @@ if (ini_get('session.use_cookies')) {
]);
}
session_destroy();
-header('Location: /');
+$return = 'https://tools.dobetternorge.no/';
+header('Location: https://dobetternorge.no/logout.php?redirect=' . urlencode($return));
exit;
diff --git a/api/profile.php b/api/profile.php
new file mode 100644
index 0000000..707366b
--- /dev/null
+++ b/api/profile.php
@@ -0,0 +1,88 @@
+ $maxChars) {
+ dbnToolsError("Field {$key} is too long.", 422, 'field_too_long');
+ }
+ return $value;
+}
+
+$displayName = profileString($input, 'display_name', 100, $currentProfile);
+$phone = profileString($input, 'phone', 40, $currentProfile);
+$addressLine1 = profileString($input, 'address_line1', 180, $currentProfile);
+$addressLine2 = profileString($input, 'address_line2', 180, $currentProfile);
+$postalCode = profileString($input, 'postal_code', 32, $currentProfile);
+$city = profileString($input, 'city', 100, $currentProfile);
+$addressRegion = profileString($input, 'address_region', 100, $currentProfile);
+$country = strtoupper(profileString($input, 'country', 2, $currentProfile));
+$preferredLanguage = dbnToolsNormalizeUiLanguage($input['preferred_language'] ?? ($_SESSION['lang'] ?? 'en'));
+$dismissPrompt = !empty($input['dismiss_prompt']);
+
+try {
+ $db = dbnmDb();
+ $stmt = $db->prepare(
+ 'UPDATE users
+ SET display_name = NULLIF(?, ""),
+ phone = NULLIF(?, ""),
+ address_line1 = NULLIF(?, ""),
+ address_line2 = NULLIF(?, ""),
+ postal_code = NULLIF(?, ""),
+ city = NULLIF(?, ""),
+ address_region = NULLIF(?, ""),
+ country = NULLIF(?, ""),
+ preferred_language = ?,
+ profile_prompt_dismissed_at = CASE
+ WHEN ? = 1 THEN COALESCE(profile_prompt_dismissed_at, NOW())
+ ELSE profile_prompt_dismissed_at
+ END
+ WHERE id = ?'
+ );
+ $stmt->execute([
+ $displayName,
+ $phone,
+ $addressLine1,
+ $addressLine2,
+ $postalCode,
+ $city,
+ $addressRegion,
+ $country,
+ $preferredLanguage,
+ $dismissPrompt ? 1 : 0,
+ $userId,
+ ]);
+
+ if ($displayName !== '') {
+ $_SESSION['dbn_tools_user_name'] = $displayName;
+ $_SESSION['dbn_tools_sso_name'] = $displayName;
+ }
+ $_SESSION['lang'] = $preferredLanguage;
+
+ dbnToolsRespond([
+ 'ok' => true,
+ 'profile' => dbnToolsMainUserProfile($userId),
+ ]);
+} catch (Throwable $e) {
+ error_log('[profile] ' . $e->getMessage());
+ dbnToolsError('Could not save profile details.', 500, 'profile_save_failed');
+}
diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css
index f7c10c2..138bd2d 100644
--- a/assets/css/dashboard.css
+++ b/assets/css/dashboard.css
@@ -171,3 +171,265 @@ body[data-dashboard-page] {
/* ── Pagination ──────────────────────────────────────────────────────── */
.dash-pager { display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; color: rgba(22, 19, 15, 0.55); font-size: 0.85rem; }
.dash-pager__actions { display: flex; gap: 0.5rem; }
+
+/* ── Section kicker ───────────────────────────────────────────────── */
+.dash-section-kicker {
+ font-size: 0.72rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: rgba(22, 19, 15, 0.5);
+ margin: 0 0 0.2rem;
+}
+
+/* ── Tier badge (bg/color set inline by PHP, base shape here) ─────── */
+.dash-tier-badge {
+ display: inline-block;
+ padding: 0.18rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ white-space: nowrap;
+}
+
+/* ── Overview bar ─────────────────────────────────────────────────── */
+.dash-overview-bar {
+ background: #fff;
+ border: 1px solid var(--dbn-line);
+ border-radius: var(--dash-radius);
+ padding: 1.1rem 1.5rem;
+ margin: 0 0 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+.dash-overview-bar__left {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ flex-wrap: wrap;
+ min-width: 0;
+}
+.dash-overview-bar__credits {
+ font-size: 1.08rem;
+ font-weight: 700;
+ color: var(--dbn-blue, #00205b);
+ white-space: nowrap;
+}
+.dash-overview-bar__meta {
+ color: #9ca3af;
+ font-size: 0.92rem;
+ white-space: nowrap;
+}
+.dash-overview-bar__meta strong { color: #374151; }
+.dash-overview-bar__right {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+.dash-overview-bar__action {
+ font-size: 0.92rem;
+ text-decoration: none;
+ padding: 7px 14px;
+ border-radius: 7px;
+ white-space: nowrap;
+ font-weight: 600;
+}
+.dash-overview-bar__action--manage {
+ color: #6b7280;
+ border: 1px solid var(--dbn-line);
+ font-weight: 400;
+}
+.dash-overview-bar__action--upgrade {
+ background: var(--dbn-blue, #00205b);
+ color: #fff;
+ font-weight: 700;
+}
+.dash-overview-bar__action--topup {
+ background: #eff6ff;
+ color: #1d4ed8;
+ border: 1px solid #bfdbfe;
+}
+
+/* ── Account section grid ─────────────────────────────────────────── */
+.dashboard-account-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1.25rem;
+ margin-top: 1.5rem;
+}
+@media (max-width: 760px) { .dashboard-account-grid { grid-template-columns: 1fr; } }
+
+.dash-account-panel {
+ background: #fff;
+ border: 1px solid var(--dbn-line);
+ border-radius: var(--dash-radius);
+ padding: 1.25rem 1.5rem;
+ box-shadow: var(--dash-shadow);
+}
+
+.dash-account-panel__head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+}
+.dash-account-panel__head h2 {
+ font-family: "Crimson Pro", "Georgia", serif;
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 0;
+ color: var(--dbn-blue, #00205b);
+}
+
+.dash-account-chip {
+ display: inline-block;
+ padding: 0.2rem 0.6rem;
+ border-radius: 999px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ background: #f3f4f6;
+ color: rgba(22, 19, 15, 0.6);
+ white-space: nowrap;
+ margin-top: 0.15rem;
+}
+
+.dash-account-list { display: flex; flex-direction: column; }
+.dash-account-list > div {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ padding: 0.55rem 0;
+ border-bottom: 1px solid var(--dbn-line);
+ font-size: 0.9rem;
+ gap: 0.5rem;
+}
+.dash-account-list > div:last-child { border-bottom: none; }
+.dash-account-list span { color: rgba(22, 19, 15, 0.55); }
+.dash-account-list strong { font-weight: 600; color: var(--dbn-ink, #16130f); text-align: right; word-break: break-all; }
+
+.dash-account-note {
+ font-size: 0.78rem;
+ color: rgba(22, 19, 15, 0.45);
+ margin-top: 1rem;
+ margin-bottom: 0;
+ line-height: 1.5;
+}
+
+/* ── Profile form ─────────────────────────────────────────────────── */
+.dash-profile-form {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem;
+}
+.dash-profile-form .span-2 { grid-column: 1 / -1; }
+.dash-profile-form label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ font-size: 0.8rem;
+ color: rgba(22, 19, 15, 0.6);
+ font-weight: 500;
+}
+.dash-profile-form input {
+ padding: 0.45rem 0.65rem;
+ border: 1px solid var(--dbn-line);
+ border-radius: var(--dash-radius-sm);
+ font: inherit;
+ font-size: 0.875rem;
+ color: var(--dbn-ink, #16130f);
+ background: var(--dbn-paper, #f6f2ea);
+ transition: border-color .15s, background .15s;
+}
+.dash-profile-form input:focus {
+ outline: none;
+ border-color: var(--dbn-blue, #00205b);
+ background: #fff;
+}
+.dash-profile-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 0.25rem;
+}
+.dash-profile-save {
+ padding: 0.45rem 1.1rem;
+ background: var(--dbn-blue, #00205b);
+ color: #fff;
+ border: none;
+ border-radius: var(--dash-radius-sm);
+ font: inherit;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background .15s;
+}
+.dash-profile-save:hover { background: #001543; }
+.dash-profile-status { font-size: 0.8rem; color: rgba(22, 19, 15, 0.55); }
+
+/* ── Onboarding prompt modal ──────────────────────────────────────── */
+.profile-prompt-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 900;
+}
+.profile-prompt-backdrop[hidden] { display: none; }
+.profile-prompt-card {
+ background: #fff;
+ border-radius: 14px;
+ padding: 2rem 2.25rem;
+ max-width: 540px;
+ width: 92%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18);
+}
+.profile-prompt-card h2 {
+ font-family: "Crimson Pro", "Georgia", serif;
+ font-size: 1.25rem;
+ font-weight: 700;
+ margin: 0.25rem 0 0.5rem;
+ color: var(--dbn-blue, #00205b);
+}
+.profile-prompt-card > p {
+ font-size: 0.875rem;
+ color: rgba(22, 19, 15, 0.6);
+ margin-bottom: 1.25rem;
+}
+.profile-prompt-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.75rem;
+ grid-column: 1 / -1;
+ margin-top: 0.5rem;
+}
+.profile-prompt-actions button[type="button"] {
+ padding: 0.45rem 1rem;
+ background: #f3f4f6;
+ color: var(--dbn-ink, #16130f);
+ border: 1px solid var(--dbn-line);
+ border-radius: var(--dash-radius-sm);
+ font: inherit;
+ font-size: 0.875rem;
+ cursor: pointer;
+}
+.profile-prompt-actions button[type="submit"] {
+ padding: 0.45rem 1.1rem;
+ background: var(--dbn-blue, #00205b);
+ color: #fff;
+ border: none;
+ border-radius: var(--dash-radius-sm);
+ font: inherit;
+ font-size: 0.875rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+.profile-prompt-actions button:hover { opacity: 0.88; }
diff --git a/billing.php b/billing.php
index f1dc944..952c46c 100644
--- a/billing.php
+++ b/billing.php
@@ -5,10 +5,7 @@ require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/PricingCatalog.php';
-if (!dbnToolsIsAuthenticated()) {
- header('Location: /?return=' . urlencode('/billing.php'));
- exit;
-}
+dbnToolsRequirePageAuth('/billing.php');
$uiLang = dbnToolsCurrentLanguage();
$isSso = dbnToolsIsFreeTier();
diff --git a/case-result.php b/case-result.php
index 7481234..5220d5f 100644
--- a/case-result.php
+++ b/case-result.php
@@ -6,10 +6,7 @@ require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/CaseStore.php';
require_once __DIR__ . '/includes/CaseResults.php';
-if (!dbnToolsIsAuthenticated()) {
- header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/'));
- exit;
-}
+dbnToolsRequirePageAuth($_SERVER['REQUEST_URI'] ?? '/');
$uiLang = dbnToolsCurrentLanguage();
$userId = (int)($_SESSION['dbn_tools_sso_uid'] ?? 0);
diff --git a/dashboard.php b/dashboard.php
index a377626..432e66d 100644
--- a/dashboard.php
+++ b/dashboard.php
@@ -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) : '';
+