v3: localization fixes, char counters & limits on all text fields, ux fixes
This commit is contained in:
@@ -6,10 +6,10 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts";
|
||||
import { API_URL, DEFAULT_PAGE_SIZE, VALIDATION } from "../config/api.ts";
|
||||
import type { Dump, PaginatedData, PublicUser } from "../model.ts";
|
||||
import {
|
||||
deserializeAuthResponse,
|
||||
@@ -89,7 +89,9 @@ function InviteButton() {
|
||||
<button type="button" className="invite-btn" onClick={generate}>
|
||||
<Trans>+ Invite someone</Trans>
|
||||
</button>
|
||||
{error && <ErrorCard title={t`Failed to generate invite`} message={error} />}
|
||||
{error && (
|
||||
<ErrorCard title={t`Failed to generate invite`} message={error} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -550,7 +552,10 @@ export function UserPublicProfile() {
|
||||
};
|
||||
|
||||
const handleDescSave = async () => {
|
||||
if (state.status !== "loaded") return;
|
||||
if (
|
||||
state.status !== "loaded" ||
|
||||
descDraft.length > VALIDATION.USER_DESCRIPTION_MAX
|
||||
) return;
|
||||
setDescSaving(true);
|
||||
setDescError(null);
|
||||
try {
|
||||
@@ -587,7 +592,9 @@ export function UserPublicProfile() {
|
||||
if (state.status === "loading") {
|
||||
return (
|
||||
<PageShell>
|
||||
<p className="page-loading"><Trans>Loading profile…</Trans></p>
|
||||
<p className="page-loading">
|
||||
<Trans>Loading profile…</Trans>
|
||||
</p>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -689,7 +696,9 @@ export function UserPublicProfile() {
|
||||
className="profile-email-btn profile-email-btn--save"
|
||||
disabled={emailSaving || !emailDraft.trim()}
|
||||
>
|
||||
{emailSaving ? <Trans>Saving…</Trans> : <Trans>Save</Trans>}
|
||||
{emailSaving
|
||||
? <Trans>Saving…</Trans>
|
||||
: <Trans>Save</Trans>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -723,7 +732,10 @@ export function UserPublicProfile() {
|
||||
)
|
||||
)}
|
||||
{avatarError && (
|
||||
<ErrorCard title={t`Failed to update avatar`} message={avatarError} />
|
||||
<ErrorCard
|
||||
title={t`Failed to update avatar`}
|
||||
message={avatarError}
|
||||
/>
|
||||
)}
|
||||
{!isOwnProfile && (
|
||||
<FollowUserButton
|
||||
@@ -754,13 +766,15 @@ export function UserPublicProfile() {
|
||||
onSubmit={handleDescSave}
|
||||
placeholder={t`Tell people about yourself…`}
|
||||
autoResize
|
||||
maxLength={VALIDATION.USER_DESCRIPTION_MAX}
|
||||
/>
|
||||
<div className="profile-description-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary"
|
||||
onClick={handleDescSave}
|
||||
disabled={descSaving}
|
||||
disabled={descSaving ||
|
||||
descDraft.length > VALIDATION.USER_DESCRIPTION_MAX}
|
||||
>
|
||||
{descSaving ? <Trans>Saving…</Trans> : <Trans>Save</Trans>}
|
||||
</button>
|
||||
@@ -842,7 +856,10 @@ export function UserPublicProfile() {
|
||||
<section className="profile-section" id="playlists">
|
||||
<div className="profile-section-header">
|
||||
<h2 className="profile-section-title">
|
||||
<Trans>Playlists ({playlists.items.length}{playlists.hasMore ? "+" : ""})</Trans>
|
||||
<Trans>
|
||||
Playlists ({playlists.items.length}
|
||||
{playlists.hasMore ? "+" : ""})
|
||||
</Trans>
|
||||
</h2>
|
||||
{isOwnProfile && (
|
||||
<NewPlaylistForm
|
||||
@@ -862,7 +879,11 @@ export function UserPublicProfile() {
|
||||
)}
|
||||
</div>
|
||||
{playlists.items.length === 0
|
||||
? <p className="empty-state"><Trans>No playlists yet.</Trans></p>
|
||||
? (
|
||||
<p className="empty-state">
|
||||
<Trans>No playlists yet.</Trans>
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<ul className="dump-feed">
|
||||
{playlists.items.map((p) => (
|
||||
@@ -927,7 +948,11 @@ function DumpList(
|
||||
<DumpCreateModal onClose={() => setCreateModalOpen(false)} />
|
||||
)}
|
||||
{dumps.length === 0
|
||||
? <p className="empty-state"><Trans>Nothing here yet.</Trans></p>
|
||||
? (
|
||||
<p className="empty-state">
|
||||
<Trans>Nothing here yet.</Trans>
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<ul className="dump-feed">
|
||||
{dumps.map((dump) => (
|
||||
@@ -945,7 +970,9 @@ function DumpList(
|
||||
</ul>
|
||||
)}
|
||||
{dumps.length > 0 && (
|
||||
<Link to={viewAllHref} className="profile-view-all"><Trans>View all →</Trans></Link>
|
||||
<Link to={viewAllHref} className="profile-view-all">
|
||||
<Trans>View all →</Trans>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
@@ -998,9 +1025,7 @@ function UpvotedDumpList(
|
||||
useEffect(() => {
|
||||
if (!profileUserId || !isOwnProfile) return;
|
||||
if (prevMyVotesRef.current === null) {
|
||||
// setVotedIds must fire here alongside prevMyVotesRef mutation; render-phase
|
||||
// isn't possible because startFading/cancelFading (below) are also setState
|
||||
// calls that cannot be invoked during render.
|
||||
// setVotedIds + prevMyVotesRef must be co-located to stay consistent.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setVotedIds(new Set(wsMyVotes));
|
||||
prevMyVotesRef.current = new Set(wsMyVotes);
|
||||
@@ -1021,8 +1046,8 @@ function UpvotedDumpList(
|
||||
const { dumpId, voterId, action } = lastVoteEvent;
|
||||
if (voterId !== profileUserId) return;
|
||||
if (action === "remove") {
|
||||
// setVotedIds + startFading must be coordinated in the same effect body
|
||||
// to guarantee a single render — render-phase can't call startFading (setState).
|
||||
// setVotedIds and startFading must fire together to avoid a render with
|
||||
// stale votedIds between the two updates.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setVotedIds((prev) => {
|
||||
const n = new Set(prev);
|
||||
@@ -1046,7 +1071,11 @@ function UpvotedDumpList(
|
||||
<h2 className="profile-section-title">{title}</h2>
|
||||
</div>
|
||||
{visibleDumps.length === 0
|
||||
? <p className="empty-state"><Trans>Nothing here yet.</Trans></p>
|
||||
? (
|
||||
<p className="empty-state">
|
||||
<Trans>Nothing here yet.</Trans>
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<ul className="dump-feed">
|
||||
{visibleDumps.map((dump) => {
|
||||
@@ -1073,7 +1102,9 @@ function UpvotedDumpList(
|
||||
</ul>
|
||||
)}
|
||||
{visibleDumps.length > 0 && (
|
||||
<Link to={viewAllHref} className="profile-view-all"><Trans>View all →</Trans></Link>
|
||||
<Link to={viewAllHref} className="profile-view-all">
|
||||
<Trans>View all →</Trans>
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user