setting, session and profile fix

This commit is contained in:
debu-2 2026-10-07 03:36:56 +05:30
parent e3d8215f2f
commit 068142ed61
7 changed files with 134 additions and 66 deletions

View file

@ -1,3 +1,4 @@
import { useEffect, useLayoutEffect, useState } from "react";
import DashboardPage from "./components/DashboardPage";
import ForgotPasswordPage from "./components/ForgotPasswordPage";
import InviteAcceptPage from "./components/InviteAcceptPage";
@ -6,24 +7,47 @@ import MobileLanding from "./components/MobileLanding";
import SignupPage from "./components/SignupPage";
import SmtpCredentialsPage from "./components/SmtpCredentialsPage";
import WebLanding from "./components/WebLanding";
import { APP_HOME_PATH, currentPath, enterApp, isLoggedIn, SESSION_GATED_PATHS } from "./auth";
function useLocationPath() {
const [path, setPath] = useState(currentPath);
useEffect(() => {
function sync() {
setPath(currentPath());
}
window.addEventListener("popstate", sync);
return () => window.removeEventListener("popstate", sync);
}, []);
return path;
}
export default function App() {
if (window.location.pathname === "/app") {
const path = useLocationPath();
const authed = isLoggedIn();
const showApp = path === APP_HOME_PATH || (authed && SESSION_GATED_PATHS.has(path));
useLayoutEffect(() => {
if (authed && SESSION_GATED_PATHS.has(currentPath())) {
enterApp();
}
}, [authed, path]);
if (showApp) {
return <DashboardPage />;
}
if (window.location.pathname === "/login") {
if (path === "/login") {
return <LoginPage />;
}
if (window.location.pathname === "/forgot-password") {
if (path === "/forgot-password") {
return <ForgotPasswordPage />;
}
if (window.location.pathname.startsWith("/invite/")) {
if (path.startsWith("/invite/")) {
return <InviteAcceptPage />;
}
if (window.location.pathname === "/signup") {
if (path === "/signup") {
return <SignupPage />;
}
if (window.location.pathname === "/smtp-credentials") {
if (path === "/smtp-credentials") {
return <SmtpCredentialsPage />;
}
return (

View file

@ -1,6 +1,9 @@
export const AUTH_TOKEN_KEY = "agmail_access_token";
export const AUTH_ACCOUNT_KEY = "agmail_account";
export const APP_HOME_PATH = "/app";
export const SESSION_GATED_PATHS = new Set(["/", "/login", "/signup", "/forgot-password"]);
let sessionRedirecting = false;
export function getAccessToken() {
@ -16,8 +19,25 @@ export function clearSession() {
sessionStorage.removeItem(AUTH_ACCOUNT_KEY);
}
export function currentPath() {
const path = window.location.pathname;
if (!path || path === "/") return "/";
return path.replace(/\/+$/, "") || "/";
}
export function enterApp(mode = "replace") {
if (currentPath() === APP_HOME_PATH && !window.location.search && !window.location.hash) {
return;
}
if (mode === "assign") {
window.location.assign(APP_HOME_PATH);
return;
}
window.history.replaceState(null, "", APP_HOME_PATH);
}
export function getBrandHomeHref() {
return isLoggedIn() ? "/app" : "/";
return isLoggedIn() ? APP_HOME_PATH : "/";
}
export function redirectSessionExpired() {
@ -26,3 +46,4 @@ export function redirectSessionExpired() {
clearSession();
window.location.replace("/?session=expired");
}

View file

@ -1,4 +1,3 @@
import { Camera, UserRound } from "lucide-react";
import { useRef, useState } from "react";
import { updateAccountProfile } from "../api";
@ -9,10 +8,18 @@ function accountInitials(account) {
return `${account?.first_name?.[0] || ""}${account?.last_name?.[0] || ""}`.toUpperCase() || "AG";
}
function displayPhone(phone) {
if (!phone) return "Not set";
return phone.replace(/^\+/, "");
}
export default function AccountProfile({ account, onUpdated }) {
const token = sessionStorage.getItem("agmail_access_token");
const inputRef = useRef(null);
const [status, setStatus] = useState({ loading: false, error: "", notice: "" });
const fullName = `${account?.first_name || ""} ${account?.last_name || ""}`.trim();
const isCompany = account?.account_type === "company";
const hasPhoto = Boolean(account?.profile_picture_url);
async function handleFileChange(event) {
const file = event.target.files?.[0];
@ -39,7 +46,7 @@ export default function AccountProfile({ account, onUpdated }) {
const updated = await updateAccountProfile({ profile_picture_url: dataUrl }, token);
sessionStorage.setItem("agmail_account", JSON.stringify(updated));
onUpdated?.(updated);
setStatus({ loading: false, error: "", notice: "Profile picture updated." });
setStatus({ loading: false, error: "", notice: "Saved" });
} catch (error) {
setStatus({ loading: false, error: error.message, notice: "" });
}
@ -51,7 +58,7 @@ export default function AccountProfile({ account, onUpdated }) {
const updated = await updateAccountProfile({ profile_picture_url: null }, token);
sessionStorage.setItem("agmail_account", JSON.stringify(updated));
onUpdated?.(updated);
setStatus({ loading: false, error: "", notice: "Profile picture removed." });
setStatus({ loading: false, error: "", notice: "Saved" });
} catch (error) {
setStatus({ loading: false, error: error.message, notice: "" });
}
@ -60,42 +67,48 @@ export default function AccountProfile({ account, onUpdated }) {
return (
<section className="account-profile-page">
<header className="account-profile-head">
<div>
<p className="account-profile-eyebrow">Account</p>
<h1>Profile</h1>
<p>Update how you appear across Gentic.</p>
</div>
<p className="account-profile-eyebrow">{isCompany ? "Company workspace" : "Personal workspace"}</p>
<h1>Profile</h1>
<p>Manage your personal information and how you appear across Gentic.</p>
<p className="account-profile-meta">
{isCompany ? "Company account" : "Personal account"} · {isCompany ? "Company" : "Individual"}
</p>
</header>
<div className="account-profile-card">
<div className="account-profile-avatar-block">
<div className="account-profile-section">
<div className="account-profile-identity">
<div className="account-profile-avatar">
{account?.profile_picture_url ? (
{hasPhoto ? (
<img src={account.profile_picture_url} alt="" />
) : (
<span>{accountInitials(account)}</span>
)}
</div>
<div className="account-profile-avatar-actions">
<button
type="button"
className="account-profile-btn"
disabled={status.loading}
onClick={() => inputRef.current?.click()}
>
<Camera size={16} />
{account?.profile_picture_url ? "Change photo" : "Upload photo"}
</button>
{account?.profile_picture_url && (
<div className="account-profile-identity-copy">
<strong>{fullName}</strong>
<div className="account-profile-identity-actions">
<button
type="button"
className="account-profile-btn subtle"
className="account-profile-link"
disabled={status.loading}
onClick={removePhoto}
onClick={() => inputRef.current?.click()}
>
Remove
{hasPhoto ? "Change photo" : "Upload photo"}
</button>
)}
{hasPhoto && (
<>
<span aria-hidden="true">·</span>
<button
type="button"
className="account-profile-link subtle"
disabled={status.loading}
onClick={removePhoto}
>
Remove
</button>
</>
)}
</div>
<input
ref={inputRef}
type="file"
@ -109,23 +122,28 @@ export default function AccountProfile({ account, onUpdated }) {
<div className="account-profile-fields">
<label>
<span>Name</span>
<div className="account-profile-readonly">
<UserRound size={16} />
{account?.first_name} {account?.last_name}
</div>
<input value={fullName} readOnly disabled />
</label>
<label>
<span>Email</span>
<div className="account-profile-readonly">{account?.email}</div>
<input value={account?.email || ""} readOnly disabled />
<small>Your Gentic email address</small>
</label>
<label>
<span>Phone</span>
<div className="account-profile-readonly">{account?.phone_number || "Not set"}</div>
<input value={displayPhone(account?.phone_number)} readOnly disabled />
</label>
</div>
{status.notice && <p className="account-profile-notice" role="status">{status.notice}</p>}
{status.error && <p className="form-error" role="alert">{status.error}</p>}
<div className="account-profile-footer">
{status.error && <p className="form-error" role="alert">{status.error}</p>}
{status.notice && !status.error && (
<p className="account-profile-saved" role="status">{status.notice}</p>
)}
{status.loading && !status.notice && (
<p className="account-profile-saved" role="status">Saving…</p>
)}
</div>
</div>
</section>
);

View file

@ -4,9 +4,9 @@ export default function Brand({ compact = false, forceHome = false }) {
const href = forceHome ? "/app" : getBrandHomeHref();
function handleClick(event) {
if (!forceHome) return;
if (!forceHome && getBrandHomeHref() !== "/app") return;
event.preventDefault();
window.location.assign("/app");
window.location.replace("/app");
}
return (

View file

@ -178,7 +178,7 @@ export default function DashboardPage() {
function logout() {
clearSession();
window.location.assign("/login");
window.location.replace("/login");
}
function closeMobileMenu() {
@ -506,7 +506,7 @@ export default function DashboardPage() {
<span className="dashboard-account-pill">{workspaceLabel}</span>
</div>
)}
{!companyActive && !["Inbox", "Outbox", "Trash"].includes(active) && (
{!companyActive && !["Inbox", "Outbox", "Trash", "Profile", "Settings"].includes(active) && (
<div className="dashboard-content-heading">
<div><p>{workspaceTitle}</p><h1>{active}</h1></div>
<span className="dashboard-account-pill">{workspaceLabel}</span>
@ -609,7 +609,7 @@ export default function DashboardPage() {
{(active === "Settings" || companyActive === "Settings") && (
<AccountSettings />
)}
{!companyActive && !["Inbox", "Outbox", "Trash", "Settings"].includes(active) && (
{!companyActive && !["Inbox", "Outbox", "Trash", "Settings", "Profile"].includes(active) && (
<section className="dashboard-empty">
<span><Sparkles size={24} /></span>
<h2>{active} is ready for you</h2>

View file

@ -13,12 +13,6 @@ export default function LoginPage() {
const [challenge, setChallenge] = useState(null);
const [otpCooldown, setOtpCooldown] = useState(0);
useEffect(() => {
if (isLoggedIn()) {
window.location.replace("/app");
}
}, []);
useEffect(() => {
if (!otpCooldown) return undefined;
const timer = window.setInterval(() => {
@ -34,6 +28,10 @@ export default function LoginPage() {
return true;
}, [status.loading, email, password, challenge, otpValid]);
if (isLoggedIn()) {
return null;
}
async function handleSubmit(event) {
event.preventDefault();
setStatus({ loading: true, error: "" });

View file

@ -1421,24 +1421,31 @@ body.compose-blank-open .mailbox-empty-cta { opacity:.42; filter:saturate(.55);
.workspace-account-menu button:hover { background:#f7f5fa; }
.workspace-account-menu button.danger { color:#b54f3d; }
.workspace-account-menu button.danger:hover { background:#fdeeed; }
.account-profile-page { max-width:720px; }
.account-profile-page { width:min(100%,800px); }
.account-profile-head { margin-bottom:22px; padding-bottom:18px; border-bottom:1px solid #ece9ee; }
.account-profile-head h1 { margin:4px 0 0; font:700 clamp(28px,3vw,34px)/1.15 "Manrope"; letter-spacing:-.04em; }
.account-profile-head p { margin:8px 0 0; color:#77758a; font-size:13px; }
.account-profile-eyebrow { margin:0; color:#7357f6; font-size:10px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.account-profile-card { margin-top:18px; padding:20px; border:1px solid #ece9ee; border-radius:16px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,.04); }
.account-profile-avatar-block { display:flex; align-items:center; gap:16px; padding-bottom:18px; border-bottom:1px solid #f0eef2; }
.account-profile-avatar { width:72px; height:72px; display:grid; place-items:center; border-radius:50%; overflow:hidden; background:linear-gradient(180deg,#ff9a82,#ff6f57); color:#2a2038; font-size:22px; font-weight:700; }
.account-profile-head > p { margin:8px 0 0; color:#77758a; font-size:13px; line-height:1.45; }
.account-profile-eyebrow { margin:0; color:#8a84a0; font-size:10px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
.account-profile-meta { margin:10px 0 0; color:#6f6b76; font-size:12px; font-weight:600; }
.account-profile-section { padding:2px 0 8px; }
.account-profile-identity { display:flex; align-items:center; gap:14px; padding-bottom:16px; margin-bottom:4px; border-bottom:1px solid #f0eef2; }
.account-profile-avatar { width:64px; height:64px; flex:0 0 64px; display:grid; place-items:center; border-radius:50%; overflow:hidden; background:linear-gradient(180deg,#ff9a82,#ff6f57); color:#2a2038; font-size:18px; font-weight:700; }
.account-profile-avatar img { width:100%; height:100%; object-fit:cover; }
.account-profile-avatar-actions { display:flex; flex-wrap:wrap; gap:8px; }
.account-profile-btn { height:36px; padding:0 12px; display:inline-flex; align-items:center; gap:8px; border:1px solid #e4e1e8; border-radius:10px; background:#fff; color:#3d3948; font-size:12px; font-weight:600; }
.account-profile-btn.subtle { color:#77758a; }
.account-profile-btn:hover:not(:disabled) { background:#f7f5fa; }
.account-profile-btn:disabled { opacity:.55; cursor:wait; }
.account-profile-fields { margin-top:18px; display:grid; gap:14px; }
.account-profile-identity-copy { min-width:0; display:grid; gap:4px; }
.account-profile-identity-copy strong { font-size:15px; font-weight:700; color:#171424; }
.account-profile-identity-actions { display:flex; align-items:center; gap:8px; }
.account-profile-link { padding:0; border:0; background:transparent; color:#c95945; font-size:12px; font-weight:700; }
.account-profile-link.subtle { color:#7a7580; font-weight:600; }
.account-profile-link:hover:not(:disabled) { text-decoration:underline; }
.account-profile-link:disabled { opacity:.55; cursor:wait; }
.account-profile-fields { margin-top:16px; display:grid; gap:16px; }
.account-profile-fields label { display:grid; gap:6px; }
.account-profile-fields label>span { color:#77758a; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
.account-profile-readonly { min-height:42px; padding:0 12px; display:flex; align-items:center; gap:8px; border:1px solid #ece9ee; border-radius:10px; background:#faf9fc; color:#3d3948; font-size:13px; }
.account-profile-notice { margin:14px 0 0; color:#3d8a5d; font-size:12px; }
.account-profile-fields input { width:100%; min-height:42px; padding:0 12px; border:1px solid #ece9ee; border-radius:10px; background:#f6f5f8; color:#3d3948; font-size:13px; }
.account-profile-fields input:disabled { color:#4a4652; cursor:default; opacity:1; }
.account-profile-fields small { color:#8c8790; font-size:11px; font-weight:500; }
.account-profile-footer { min-height:20px; margin-top:14px; display:flex; justify-content:flex-end; }
.account-profile-saved { margin:0; color:#3d8a5d; font-size:12px; font-weight:600; }
.workspace-account div { min-width:0; display:grid; text-align:left; }
.workspace-account strong,.workspace-account small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.workspace-account strong { color:#241f2b; font-size:12px; font-weight:600; }