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) : ''; +