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; user-select: none;
} }
.mx-tweet-avatar--lg {
width: 56px;
height: 56px;
font-size: 1.25rem;
}
.mx-compose-body { flex: 1; } .mx-compose-body { flex: 1; }
.mx-compose-textarea, .mx-edit-textarea { .mx-compose-textarea, .mx-edit-textarea {
@@ -964,4 +970,6 @@ html, body {
} }
.mx-header { padding: 0.75rem 1rem; } .mx-header { padding: 0.75rem 1rem; }
.mx-detail { padding: 0.875rem 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 { userId: currentUserId } = useContext(AuthCtx);
const { follow, unfollow, isPending } = useFollowUser(userId); const { follow, unfollow, isPending } = useFollowUser(userId);
const { data: user, isLoading, isError } = useQuery({ const { data: user, isLoading, isError } = useQuery({
@@ -1228,30 +1228,38 @@ function UserDetail({ userId }: { userId: string }) {
if (isLoading) return <Spinner />; if (isLoading) return <Spinner />;
if (isError || !user) return <ErrorBanner message="Could not load user" />; 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; const amIFollowing = user.amIFollowing ?? false;
return ( return (
<div className="mx-detail"> <div className="mx-detail">
<div className="mx-detail-header"> {!isStandalone && (
<a href="/users" className="mx-back-btn"> <div className="mx-detail-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> <a href="/users" className="mx-back-btn">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" /> <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
</svg> <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
Back </svg>
</a> Back
</div> </a>
</div>
)}
<div className="mx-detail-body"> <div className="mx-detail-body">
<div className="mx-detail-author"> <div className="mx-detail-author">
<div className="mx-tweet-avatar"> <div className="mx-tweet-avatar mx-tweet-avatar--lg">
<span>M</span> <span>{user.email?.[0]?.toUpperCase() ?? "M"}</span>
</div> </div>
<div style={{ flex: 1 }}> <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> <span className="mx-tweet-handle">{user.email}</span>
{canFollow && ( {canFollow && (
<FollowButton amIFollowing={amIFollowing} isPending={isPending} onToggle={amIFollowing ? unfollow : follow} /> <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>
<div style={{ fontSize: "0.85rem", color: "var(--mx-muted)", marginTop: "6px", display: "flex", gap: "16px" }}> <div style={{ fontSize: "0.85rem", color: "var(--mx-muted)", marginTop: "6px", display: "flex", gap: "16px" }}>
<span><strong>{user.followerCount ?? 0}</strong> followers</span> <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 ───────────────────────────────────────────────────────── // ── Mobile bottom nav ─────────────────────────────────────────────────────────
function MobileNav({ function MobileNav({
@@ -1276,6 +1303,7 @@ function MobileNav({
const onFeedPage = page === "feed" || page === "tweet"; const onFeedPage = page === "feed" || page === "tweet";
const onFollowingPage = page === "following"; const onFollowingPage = page === "following";
const onUsersPage = page === "users" || page === "user-detail"; const onUsersPage = page === "users" || page === "user-detail";
const onProfilePage = page === "profile";
return ( return (
<nav className="mx-mobile-nav"> <nav className="mx-mobile-nav">
@@ -1328,6 +1356,16 @@ function MobileNav({
</svg> </svg>
<span>Users</span> <span>Users</span>
</a> </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> </nav>
); );
} }
@@ -1379,6 +1417,7 @@ function App() {
const onFeedPage = page === "feed" || page === "tweet"; const onFeedPage = page === "feed" || page === "tweet";
const onFollowingPage = page === "following"; const onFollowingPage = page === "following";
const onUsersPage = page === "users" || page === "user-detail"; const onUsersPage = page === "users" || page === "user-detail";
const onProfilePage = page === "profile";
function renderMain() { function renderMain() {
switch (page) { switch (page) {
@@ -1419,6 +1458,15 @@ function App() {
<UserDetail userId={profileUserId!} /> <UserDetail userId={profileUserId!} />
</> </>
); );
case "profile":
return (
<>
<header className="mx-header">
<h1 className="mx-header-title">My Profile</h1>
</header>
<MyProfile />
</>
);
default: default:
return ( return (
<> <>
@@ -1475,6 +1523,12 @@ function App() {
</svg> </svg>
Users Users
</a> </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> </nav>
<div className="mx-sidebar-footer"> <div className="mx-sidebar-footer">
{email ? ( {email ? (

View File

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

View File

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