v3: lots of small UI tweaks and fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s
This commit is contained in:
@@ -53,6 +53,7 @@ import { friendlyFetchError } from "../utils/apiError.ts";
|
||||
import { TextEditor } from "../components/TextEditor.tsx";
|
||||
import { Markdown } from "../components/Markdown.tsx";
|
||||
import { ChangePasswordModal } from "../components/ChangePasswordModal.tsx";
|
||||
import { TabBar } from "../components/TabBar.tsx";
|
||||
|
||||
function InviteButton() {
|
||||
const { authFetch } = useAuth();
|
||||
@@ -145,6 +146,7 @@ type InviteTreeState =
|
||||
| { status: "loaded"; entries: InviteTreeEntry[] };
|
||||
|
||||
type InviteTreeNode = InviteTreeEntry & { children: InviteTreeNode[] };
|
||||
type ProfileTab = "dumps" | "playlists" | "followed" | "invitees" | "settings";
|
||||
|
||||
function buildInviteTree(
|
||||
entries: InviteTreeEntry[],
|
||||
@@ -282,9 +284,7 @@ export function UserPublicProfile() {
|
||||
const [emailError, setEmailError] = useState<string | null>(null);
|
||||
const prevMyVotesRef = useRef<Set<string> | null>(null);
|
||||
|
||||
const [tab, setTab] = useState<
|
||||
"dumps" | "playlists" | "followed" | "invitees" | "settings"
|
||||
>("dumps");
|
||||
const [tab, setTab] = useState<ProfileTab>("dumps");
|
||||
const [changePasswordOpen, setChangePasswordOpen] = useState(false);
|
||||
const [followedState, setFollowedState] = useState<FollowedState>(null);
|
||||
const [inviteTreeState, setInviteTreeState] = useState<InviteTreeState>(null);
|
||||
@@ -782,107 +782,112 @@ export function UserPublicProfile() {
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="profile-username">{profileUser.username}</h1>
|
||||
{profileUser.invitedByUsername
|
||||
? (
|
||||
<p className="profile-invited-by">
|
||||
<Trans>invited by</Trans>{" "}
|
||||
<Link
|
||||
to={`/users/${profileUser.invitedByUsername}`}
|
||||
className="profile-invited-by-link"
|
||||
>
|
||||
@{profileUser.invitedByUsername}
|
||||
</Link>
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className="profile-invited-by profile-invited-by--founding">
|
||||
O.G.
|
||||
</p>
|
||||
)}
|
||||
{isOwnProfile && (
|
||||
emailEditing
|
||||
<div className="profile-info">
|
||||
<div className="profile-info-scroll">
|
||||
<h1 className="profile-username">{profileUser.username}</h1>
|
||||
{profileUser.invitedByUsername
|
||||
? (
|
||||
<form
|
||||
className="profile-email-editor"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleEmailSave();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
className="profile-email-input"
|
||||
value={emailDraft}
|
||||
onChange={(e) => setEmailDraft(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") setEmailEditing(false);
|
||||
}}
|
||||
disabled={emailSaving}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="profile-email-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="profile-email-btn profile-email-btn--save"
|
||||
disabled={emailSaving || !emailDraft.trim()}
|
||||
>
|
||||
{emailSaving
|
||||
? <Trans>Saving…</Trans>
|
||||
: <Trans>Save</Trans>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="profile-email-btn profile-email-btn--cancel"
|
||||
onClick={() => setEmailEditing(false)}
|
||||
disabled={emailSaving}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
<ErrorCard title={t`Failed to save`} message={emailError} />
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
: (
|
||||
<p
|
||||
className="profile-email-display"
|
||||
onClick={() => {
|
||||
setEmailDraft(me?.email ?? "");
|
||||
setEmailError(null);
|
||||
setEmailEditing(true);
|
||||
}}
|
||||
title="Edit email"
|
||||
>
|
||||
{me?.email ?? t`Add email…`}
|
||||
<span className="profile-description-edit-btn" aria-hidden>
|
||||
✎
|
||||
</span>
|
||||
<p className="profile-invited-by">
|
||||
<Trans>invited by</Trans>{" "}
|
||||
<Link
|
||||
to={`/users/${profileUser.invitedByUsername}`}
|
||||
className="profile-invited-by-link"
|
||||
>
|
||||
@{profileUser.invitedByUsername}
|
||||
</Link>
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
{avatarError && (
|
||||
<ErrorCard
|
||||
title={t`Failed to update avatar`}
|
||||
message={avatarError}
|
||||
/>
|
||||
)}
|
||||
{!isOwnProfile && (
|
||||
<FollowUserButton
|
||||
targetUserId={profileUser.id}
|
||||
targetUsername={profileUser.username}
|
||||
/>
|
||||
)}
|
||||
{isOwnProfile && (
|
||||
<div className="profile-own-actions">
|
||||
<InviteButton />
|
||||
<button type="button" className="btn-border" onClick={logout}>
|
||||
<Trans>Log out</Trans>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
: (
|
||||
<p className="profile-invited-by profile-invited-by--founding">
|
||||
O.G.
|
||||
</p>
|
||||
)}
|
||||
{isOwnProfile && (
|
||||
emailEditing
|
||||
? (
|
||||
<form
|
||||
className="profile-email-editor"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleEmailSave();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
className="profile-email-input"
|
||||
value={emailDraft}
|
||||
onChange={(e) => setEmailDraft(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") setEmailEditing(false);
|
||||
}}
|
||||
disabled={emailSaving}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="profile-email-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="profile-email-btn profile-email-btn--save"
|
||||
disabled={emailSaving || !emailDraft.trim()}
|
||||
>
|
||||
{emailSaving
|
||||
? <Trans>Saving…</Trans>
|
||||
: <Trans>Save</Trans>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="profile-email-btn profile-email-btn--cancel"
|
||||
onClick={() => setEmailEditing(false)}
|
||||
disabled={emailSaving}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
<ErrorCard
|
||||
title={t`Failed to save`}
|
||||
message={emailError}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
: (
|
||||
<p
|
||||
className="profile-email-display"
|
||||
onClick={() => {
|
||||
setEmailDraft(me?.email ?? "");
|
||||
setEmailError(null);
|
||||
setEmailEditing(true);
|
||||
}}
|
||||
title="Edit email"
|
||||
>
|
||||
{me?.email ?? t`Add email…`}
|
||||
<span className="profile-description-edit-btn" aria-hidden>
|
||||
✎
|
||||
</span>
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
{avatarError && (
|
||||
<ErrorCard
|
||||
title={t`Failed to update avatar`}
|
||||
message={avatarError}
|
||||
/>
|
||||
)}
|
||||
{!isOwnProfile && (
|
||||
<FollowUserButton
|
||||
targetUserId={profileUser.id}
|
||||
targetUsername={profileUser.username}
|
||||
/>
|
||||
)}
|
||||
{isOwnProfile && (
|
||||
<div className="profile-own-actions">
|
||||
<InviteButton />
|
||||
<button type="button" className="btn-border" onClick={logout}>
|
||||
<Trans>Log out</Trans>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -958,47 +963,21 @@ export function UserPublicProfile() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="feed-sort-scroller profile-tabs-scroller">
|
||||
<div className="profile-tabs feed-sort">
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${tab === "dumps" ? " active" : ""}`}
|
||||
onClick={() => setTab("dumps")}
|
||||
>
|
||||
<Trans>Dumps</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${tab === "playlists" ? " active" : ""}`}
|
||||
onClick={() => setTab("playlists")}
|
||||
>
|
||||
<Trans>Playlists</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${tab === "followed" ? " active" : ""}`}
|
||||
onClick={() => setTab("followed")}
|
||||
>
|
||||
<Trans>Followed</Trans>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${tab === "invitees" ? " active" : ""}`}
|
||||
onClick={() => setTab("invitees")}
|
||||
>
|
||||
<Trans>Invitees</Trans>
|
||||
</button>
|
||||
{isOwnProfile && (
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-sort-btn${tab === "settings" ? " active" : ""}`}
|
||||
onClick={() => setTab("settings")}
|
||||
>
|
||||
<Trans>Settings</Trans>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TabBar
|
||||
tabs={[
|
||||
{ key: "dumps", label: <Trans>Dumps</Trans> },
|
||||
{ key: "playlists", label: <Trans>Playlists</Trans> },
|
||||
{ key: "followed", label: <Trans>Followed</Trans> },
|
||||
{ key: "invitees", label: <Trans>Invitees</Trans> },
|
||||
...(isOwnProfile
|
||||
? [{ key: "settings" as const, label: <Trans>Settings</Trans> }]
|
||||
: []),
|
||||
]}
|
||||
activeTab={tab}
|
||||
onChange={(key) => setTab(key)}
|
||||
className="profile-tabs-scroller"
|
||||
innerClassName="profile-tabs"
|
||||
/>
|
||||
|
||||
{tab === "dumps" && (
|
||||
<div className="profile-columns">
|
||||
@@ -1329,7 +1308,9 @@ function DumpList(
|
||||
className="new-playlist-toggle"
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
+<span className="btn-new-label"><Trans> New dump</Trans></span>
|
||||
+<span className="btn-new-label">
|
||||
<Trans>New dump</Trans>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user