Added /profile to see your profile

This commit is contained in:
2026-04-05 14:50:50 -04:00
parent 4b36131183
commit 4ec41ad4b3
4 changed files with 80 additions and 13 deletions

View File

@@ -303,6 +303,12 @@ html, body {
user-select: none;
}
.mx-tweet-avatar--lg {
width: 56px;
height: 56px;
font-size: 1.25rem;
}
.mx-compose-body { flex: 1; }
.mx-compose-textarea, .mx-edit-textarea {
@@ -964,4 +970,6 @@ html, body {
}
.mx-header { padding: 0.75rem 1rem; }
.mx-detail { padding: 0.875rem 1rem; }
/* 5-item nav: slightly smaller labels so nothing wraps */
.mx-mobile-nav-item { font-size: 0.6rem; }
}

View File

@@ -1208,7 +1208,7 @@ function UserList() {
);
}
function UserDetail({ userId }: { userId: string }) {
function UserDetail({ userId, isStandalone = false }: { userId: string; isStandalone?: boolean }) {
const { userId: currentUserId } = useContext(AuthCtx);
const { follow, unfollow, isPending } = useFollowUser(userId);
const { data: user, isLoading, isError } = useQuery({
@@ -1228,30 +1228,38 @@ function UserDetail({ userId }: { userId: string }) {
if (isLoading) return <Spinner />;
if (isError || !user) return <ErrorBanner message="Could not load user" />;
const canFollow = !!currentUserId && currentUserId !== userId;
const isOwnProfile = currentUserId === userId;
const canFollow = !!currentUserId && !isOwnProfile;
const amIFollowing = user.amIFollowing ?? false;
return (
<div className="mx-detail">
<div className="mx-detail-header">
<a href="/users" className="mx-back-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
Back
</a>
</div>
{!isStandalone && (
<div className="mx-detail-header">
<a href="/users" className="mx-back-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
Back
</a>
</div>
)}
<div className="mx-detail-body">
<div className="mx-detail-author">
<div className="mx-tweet-avatar">
<span>M</span>
<div className="mx-tweet-avatar mx-tweet-avatar--lg">
<span>{user.email?.[0]?.toUpperCase() ?? "M"}</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
<div style={{ display: "flex", alignItems: "center", gap: "12px", flexWrap: "wrap" }}>
<span className="mx-tweet-handle">{user.email}</span>
{canFollow && (
<FollowButton amIFollowing={amIFollowing} isPending={isPending} onToggle={amIFollowing ? unfollow : follow} />
)}
{isOwnProfile && isStandalone && (
<a href="/sign-out" className="mx-btn-cancel" style={{ textDecoration: "none", fontSize: "0.8rem" }}>
Sign out
</a>
)}
</div>
<div style={{ fontSize: "0.85rem", color: "var(--mx-muted)", marginTop: "6px", display: "flex", gap: "16px" }}>
<span><strong>{user.followerCount ?? 0}</strong> followers</span>
@@ -1264,6 +1272,25 @@ function UserDetail({ userId }: { userId: string }) {
);
}
function MyProfile() {
const { userId } = useContext(AuthCtx);
if (!userId) {
return (
<div className="mx-empty">
<div className="mx-empty-icon"></div>
<p className="mx-empty-title">Your profile</p>
<p className="mx-empty-sub">
<a href="/sign-in" style={{ color: "var(--mx-accent)", textDecoration: "none" }}>Sign in</a>
{" "}to view your profile.
</p>
</div>
);
}
return <UserDetail userId={userId} isStandalone />;
}
// ── Mobile bottom nav ─────────────────────────────────────────────────────────
function MobileNav({
@@ -1276,6 +1303,7 @@ function MobileNav({
const onFeedPage = page === "feed" || page === "tweet";
const onFollowingPage = page === "following";
const onUsersPage = page === "users" || page === "user-detail";
const onProfilePage = page === "profile";
return (
<nav className="mx-mobile-nav">
@@ -1328,6 +1356,16 @@ function MobileNav({
</svg>
<span>Users</span>
</a>
<a
href="/profile"
className={`mx-mobile-nav-item${onProfilePage ? " mx-mobile-nav-item--active" : ""}`}
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
<span>Profile</span>
</a>
</nav>
);
}
@@ -1379,6 +1417,7 @@ function App() {
const onFeedPage = page === "feed" || page === "tweet";
const onFollowingPage = page === "following";
const onUsersPage = page === "users" || page === "user-detail";
const onProfilePage = page === "profile";
function renderMain() {
switch (page) {
@@ -1419,6 +1458,15 @@ function App() {
<UserDetail userId={profileUserId!} />
</>
);
case "profile":
return (
<>
<header className="mx-header">
<h1 className="mx-header-title">My Profile</h1>
</header>
<MyProfile />
</>
);
default:
return (
<>
@@ -1475,6 +1523,12 @@ function App() {
</svg>
Users
</a>
<a className={`mx-nav-item${onProfilePage ? " mx-nav-active" : ""}`} href="/profile">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
</svg>
Profile
</a>
</nav>
<div className="mx-sidebar-footer">
{email ? (

View File

@@ -21,6 +21,10 @@ defmodule MixerWeb.PageController do
render_spa(conn, %{page: "following", tweet_id: nil, user_id: nil})
end
def profile(conn, _params) do
render_spa(conn, %{page: "profile", tweet_id: nil, user_id: nil})
end
def users_index(conn, _params) do
render_spa(conn, %{page: "users", tweet_id: nil, user_id: nil})
end

View File

@@ -41,6 +41,7 @@ defmodule MixerWeb.Router do
get "/feed", PageController, :index
get "/feed/:tweet_id", PageController, :show
get "/following", PageController, :following
get "/profile", PageController, :profile
get "/users", PageController, :users_index
get "/users/:user_id", PageController, :user_show
post "/rpc/run", AshTypescriptRpcController, :run