v3: added site-wide categories, added admin category management, various visual fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m52s

This commit is contained in:
khannurien
2026-06-28 16:23:44 +00:00
parent c8c7b05c25
commit fae25f3e6c
44 changed files with 2144 additions and 399 deletions

View File

@@ -37,6 +37,7 @@ import { PlaylistCard } from "../components/PlaylistCard.tsx";
import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
import { CategoryManager } from "../components/CategoryManager.tsx";
import { useAuth } from "../hooks/useAuth.ts";
import { useDocumentTitle } from "../hooks/useDocumentTitle.ts";
import { useTheme } from "../hooks/useTheme.ts";
@@ -227,9 +228,16 @@ export function UserPublicProfile() {
// `settings` is own-profile only; `manage` requires the user:manage permission
// on someone else's profile. Invalid tabs fall back to "dumps".
const canManageUsers = can(me, "user:manage");
const canManageCategories = can(me, "category:manage");
// The "manage" tab is an admin/moderation area. On someone else's profile it
// holds per-user tools (role management); on your own it holds global tools
// (category management, with more to come).
const showManageTab = isOwnProfile
? canManageCategories
: canManageUsers;
const availableTabs: ProfileTab[] = PROFILE_TABS.filter((t) => {
if (t === "settings") return isOwnProfile;
if (t === "manage") return !isOwnProfile && canManageUsers;
if (t === "manage") return showManageTab;
return true;
});
const [tab, setTab] = useTabParam<ProfileTab>(availableTabs, "dumps");
@@ -780,7 +788,10 @@ export function UserPublicProfile() {
</div>
<div className="profile-info">
<div className="profile-info-scroll">
<h1 className="profile-username">{profileUser.username}</h1>
<div className="profile-username-row">
<h1 className="profile-username">{profileUser.username}</h1>
<RoleChip role={profileUser.role} />
</div>
{profileUser.invitedByUsername
? (
<p className="profile-invited-by">
@@ -893,7 +904,7 @@ export function UserPublicProfile() {
...(isOwnProfile
? [{ key: "settings" as const, label: <Trans>Settings</Trans> }]
: []),
...(!isOwnProfile && canManageUsers
...(showManageTab
? [{ key: "manage" as const, label: <Trans>Manage</Trans> }]
: []),
]}
@@ -1263,6 +1274,15 @@ export function UserPublicProfile() {
</div>
</section>
)}
{tab === "manage" && isOwnProfile && canManageCategories && (
<section className="profile-section">
<h2 className="profile-section-title">
<Trans>Categories</Trans>
</h2>
<CategoryManager />
</section>
)}
</PageShell>
);
}
@@ -1529,6 +1549,14 @@ const ROLE_OPTIONS: { value: Role; label: React.ReactNode }[] = [
{ value: "admin", label: <Trans>Admin</Trans> },
];
// Role badge shown next to the username. The default "user" role is implicit,
// so only elevated roles get a chip to avoid labelling every profile.
function RoleChip({ role }: { role: Role }) {
if (role === "user") return null;
const label = ROLE_OPTIONS.find((o) => o.value === role)?.label;
return <span className={`profile-role-chip profile-role-chip--${role}`}>{label}</span>;
}
interface RoleSwitcherProps {
userId: string;
currentRole: Role;