Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

MemPalace wing: `keepsimple` (protocol lives in `~/.claude/CLAUDE.md`).

Human-readable agent guidelines live in `AGENTS.md` next to this file; this file is the machine-facing version. See `AGENTS.md` for repo conventions, build/test commands, and contribution rules.
Human-readable agent guidelines live in `AGENTS.md` next to this file; this file is the machine-facing version. See `AGENTS.md` for repo conventions, build/test commands, and contribution rules — imported below so it loads automatically.

@AGENTS.md

## Code search — prefer CodeGraph over Grep

Expand Down
16 changes: 14 additions & 2 deletions src/api/library/tag/getTagsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@ export interface GetTagsListResponse {
data: ITag[];
}

export const getTagsList = async (): Promise<GetTagsListResponse> => {
// Tags are owner-scoped: each is stamped with `user` on create. The default
// GET /api/tags returns every account's tags, so always filter by the current
// user's id. Without an id there's nothing safe to return — refuse rather than
// fall back to the unscoped list, which would leak other accounts' tags.
export const getTagsList = async (
userId?: number | string,
): Promise<GetTagsListResponse> => {
if (userId == null || userId === '') {
return { data: [] };
}

try {
const { data } = await axiosInstance.get<GetTagsListResponse>('/api/tags');
const { data } = await axiosInstance.get<GetTagsListResponse>('/api/tags', {
params: { 'filters[user][id][$eq]': userId },
});

return data;
} catch (error) {
Expand Down
21 changes: 21 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { TRouter } from '@local-types/global';
import useGlobals from '@hooks/useGlobals';
import { useIsWidthLessThan } from '@hooks/useScreenSize';

import { getMyLibrary } from '@api/library/getMyLibrary';
import { userInfoUpdate } from '@api/settings';
import { getMyInfo } from '@api/strapi';

Expand Down Expand Up @@ -51,6 +52,24 @@ const Header: FC = () => {
const canCreateLibrary =
accountData?.featureNames?.includes('can-create-library') ?? false;

// "My Library" is only reachable once a library exists, or could be
// bootstrapped by a flag-holder. With neither, the user has no library page,
// so the dropdown item is disabled. Check via the owner-scoped lookup.
const [hasLibrary, setHasLibrary] = useState(false);
useEffect(() => {
if (!accountData?.id) {
setHasLibrary(false);
return;
}
let cancelled = false;
getMyLibrary(accountData.id).then(lib => {
if (!cancelled) setHasLibrary(lib !== null);
});
return () => {
cancelled = true;
};
}, [accountData?.id]);

useEffect(() => {
const storedToken = localStorage.getItem('accessToken');
setToken(storedToken);
Expand Down Expand Up @@ -167,6 +186,7 @@ const Header: FC = () => {
userImage={accountData?.picture}
handleOpenSettings={handleOpenSettings}
canCreateLibrary={canCreateLibrary}
hasLibrary={hasLibrary}
hideDropdown={isOpenedSidebar}
hideUsername
/>
Expand Down Expand Up @@ -247,6 +267,7 @@ const Header: FC = () => {
userImage={accountData?.picture}
handleOpenSettings={handleOpenSettings}
canCreateLibrary={canCreateLibrary}
hasLibrary={hasLibrary}
/>
)}
</div>
Expand Down
17 changes: 15 additions & 2 deletions src/components/UserProfile/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type UserProfileProps = {
hideDropdown?: boolean;
hideUsername?: boolean;
canCreateLibrary?: boolean;
hasLibrary?: boolean;
setAccountData?: (updater: (prev: boolean) => boolean) => void;
setOpenLoginModal?: (openModal: boolean) => void;
handleOpenSettings?: () => void;
Expand Down Expand Up @@ -56,6 +57,7 @@ const UserProfile: FC<UserProfileProps> = ({
hideDropdown,
hideUsername,
canCreateLibrary,
hasLibrary,
setAccountData,
setOpenLoginModal,
handleOpenSettings,
Expand Down Expand Up @@ -84,10 +86,15 @@ const UserProfile: FC<UserProfileProps> = ({
handleOpenSettings?.();
}, [handleOpenSettings]);

// With neither an existing library nor create permission, the user has no
// library page to open, so the item is inert.
const myLibraryDisabled = !hasLibrary && !canCreateLibrary;

const handleMyLibrary = useCallback(() => {
if (myLibraryDisabled) return;
setIsDropdownOpen(false);
router.push(`/library/${username}`);
}, [router, username]);
}, [router, username, myLibraryDisabled]);

// A library has no standalone create step — it's bootstrapped on the owner's
// own page once they add content (gated server-side by the same feature
Expand Down Expand Up @@ -174,7 +181,13 @@ const UserProfile: FC<UserProfileProps> = ({
{isDropdownOpen && isAccessTokenExist && (
<div className={styles.dropdown} onClick={e => e.stopPropagation()}>
{isLibraryEnabled() && username && (
<div className={styles.menuItem} onClick={handleMyLibrary}>
<div
className={cn(styles.menuItem, {
[styles.disabled]: myLibraryDisabled,
})}
onClick={handleMyLibrary}
aria-disabled={myLibraryDisabled}
>
<LibraryIcon
width={20}
height={11}
Expand Down
2 changes: 2 additions & 0 deletions src/components/library/molecules/Button/Button.module.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.button {
gap: 8px;
height: 44px;
box-sizing: border-box;
border: none;
cursor: pointer;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

.trigger {
width: 100%;
height: 44px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
background: var(--white);
border: 1px solid var(--brown-border);
border-radius: 4px;
padding: 12px 16px;
padding: 0 16px;
cursor: pointer;
color: var(--black);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

.trigger {
width: 100%;
height: 44px;
box-sizing: border-box;
padding: 0 0 0 12px;
display: flex;
align-items: center;
Expand Down Expand Up @@ -38,7 +40,6 @@
.text {
flex: 1;
text-align: left;
padding: 12px 0;
}

.iconWrapper {
Expand Down
3 changes: 2 additions & 1 deletion src/components/library/molecules/Input/Input.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

.input {
width: 100%;
height: 35px;
height: 44px;
color: var(--black);
font-size: 16px;
padding: 0 16px;
background: var(--white);
border: 1px solid var(--gray-100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
top: 18px;
left: 46px;
right: 46px;
width: 79.5%;
height: 1px;
background: var(--gray-100);
overflow: hidden;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export function AddObjectModal(props: AddObjectModalProps): JSX.Element {

useEffect(() => {
let cancelled = false;
getTagsList().then(res => {
getTagsList(accountData?.id).then(res => {
if (cancelled) return;
const opts: TagOption[] = res.data.map(t => ({
id: t.id,
Expand All @@ -293,7 +293,7 @@ export function AddObjectModal(props: AddObjectModalProps): JSX.Element {
return () => {
cancelled = true;
};
}, []);
}, [accountData?.id]);

// Preset the object's existing tags exactly once, from the object's OWN
// populated tag data — not by filtering the fetched options. An unpublished
Expand Down
6 changes: 5 additions & 1 deletion src/components/library/organisms/LibraryCard/LibraryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ export function LibraryCard(props: LibraryCardProps): JSX.Element {
const router = useRouter();

const handleViewLibrary = () => {
router.push(`/library/${username ?? id}`);
// Route by numeric id, not username: the route resolver short-circuits a
// numeric param to a findOne-by-id, sidestepping the username→id filter
// lookup that the public API currently 500s on. Falls back to username only
// if an id is somehow absent.
router.push(`/library/${id ?? username}`);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
}
}

// Guest view of someone else's library: the divider is dropped, so restore the
// vertical breathing room on the controls row itself.
.controlsGuest {
padding: 24px 10px;
}

// Visitor / guest-preview banner that replaces the owner's shelf controls.
.welcome {
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,7 @@ export function LibraryToolbar(props: LibraryToolbarProps): JSX.Element {
if (!isOwner) {
return (
<div className={classNames(styles.toolbar, className)}>
<div className={styles.divider} />

<div className={styles.controls}>
<div className={classNames(styles.controls, styles.controlsGuest)}>
<div className={styles.welcome}>
<Text
variant={TypographyVariant.TitleSecondaryBold}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,6 @@
gap: 8px;
}

// `cursor: pointer` is scoped here — Tag elsewhere stays presentational.
.tag {
cursor: pointer;
}

.destination {
display: flex;
flex-direction: row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,10 +569,8 @@ export function ObjectOverviewModal(
{tagsList.map(t => (
<Tag
key={t.id}
className={styles.tag}
label={t.attributes.name}
color={t.attributes.color}
onClick={isOwner ? handleEdit : undefined}
/>
))}
</div>
Expand Down
17 changes: 10 additions & 7 deletions src/components/library/organisms/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function Sidebar() {
(isMyLibrary ? accountData?.picture : undefined);
const aboutAuthorText = stripHtml(currentOwner?.aboutMe);

// Owner sees their full, editable tag palette; a visitor sees the tags
// Owner sees their full tag palette; a true visitor sees only the tags
// actually used on this library's objects — no cross-account tag fetch.
const libraryTags = useMemo(() => {
const byName = new Map<string, { name: string; color: string }>();
Expand All @@ -186,7 +186,10 @@ export function Sidebar() {
return Array.from(byName.values());
}, [currentShelves]);

const displayedTags = canEdit
// Show the owner's palette whenever it's their own library — including guest
// mode (only an owner can toggle that, so we always have their tags loaded).
// Editing stays gated on `canEdit`, so guest preview shows them read-only.
const displayedTags = isMyLibrary
? tags.map(t => ({ name: t.attributes.name, color: t.attributes.color }))
: libraryTags;

Expand Down Expand Up @@ -228,7 +231,7 @@ export function Sidebar() {
};

await createTag(body);
const { data } = await getTagsList();
const { data } = await getTagsList(accountData?.id);

setTags(data);
} catch (error) {
Expand All @@ -251,7 +254,7 @@ export function Sidebar() {
};

await updateTag(selectedTag.id, body);
const { data } = await getTagsList();
const { data } = await getTagsList(accountData?.id);

setTags(data);
setIsOpenTagModal(null);
Expand All @@ -267,7 +270,7 @@ export function Sidebar() {

try {
await deleteTag(selectedTag.id);
const { data } = await getTagsList();
const { data } = await getTagsList(accountData?.id);

setTags(data);
setIsOpenTagModal(null);
Expand Down Expand Up @@ -308,13 +311,13 @@ export function Sidebar() {
// fresh page load until the user mutates a tag.
useEffect(() => {
let cancelled = false;
getTagsList().then(({ data }) => {
getTagsList(accountData?.id).then(({ data }) => {
if (!cancelled) setTags(data);
});
return () => {
cancelled = true;
};
}, [setTags]);
}, [setTags, accountData?.id]);

// Hide the right panel entirely when the owner lacks permission to create a
// library — the page shows only the centered no-permission message.
Expand Down
23 changes: 23 additions & 0 deletions src/layouts/library/Home/Home.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@
}
}

.searchGroup {
display: flex;
align-items: center;
gap: 12px;

@media (max-width: 590px) {
width: 100%;
flex-direction: column;
align-items: stretch;
}
}

.input {
max-width: 348px;
border-radius: 6px;
Expand All @@ -77,6 +89,17 @@
top: 9px;
}
}

.createButton {
flex-shrink: 0;
white-space: nowrap;

// The shared plus icon ships a hardcoded brown fill, which disappears on
// the brown primary button — force it (and the label) white.
svg path {
fill: var(--white);
}
}
}

.content {
Expand Down
Loading
Loading