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
Binary file added public/assets/library/library-ultrawide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/api/library/getLibrariesPaginated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,25 @@ import { LIBRARY_CARD_POPULATE } from '@api/library/libraryCardPopulate';
export const getLibrariesPaginated = async (
page = 1,
pageSize = 8,
query = '',
): Promise<StrapiLibrariesResponse | null> => {
try {
// Libraries are owner-scoped and shown as "<username>'s library", so search
// the owner's username — the same relation filter `getLibraryIdByUsername`
// uses for routing. `$containsi` = case-insensitive partial match. An `$or`
// that also covered the raw `name` field silently returned no rows through
// this controller, so keep to the single proven relation filter.
const trimmed = query.trim();
const filters = trimmed
? { 'filters[user][username][$containsi]': trimmed }
: {};

const { data } = await axiosInstance.get<StrapiLibrariesResponse>(
'/api/libraries',
{
params: {
...LIBRARY_CARD_POPULATE,
...filters,
'pagination[page]': page,
'pagination[pageSize]': pageSize,
},
Expand Down
7 changes: 5 additions & 2 deletions src/api/library/libraryCardPopulate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Populate the relations the home/sidebar library cards need: avatar (image),
// user (for the `/library/[username]` URL), and shelves + their objects (so the
// per-type counts reflect object totals, not shelf totals).
// user (for the `/library/[username]` URL), shelves + their objects (so the
// per-type counts reflect object totals, not shelf totals), and libraryDetails
// (the `aboutLibrary` component field shown as the card's "About" blurb —
// components aren't returned unless explicitly populated).
export const LIBRARY_CARD_POPULATE = {
'populate[avatar]': true,
'populate[user]': true,
'populate[singleShelves][populate][objects]': true,
'populate[libraryDetails]': true,
} as const;
10 changes: 10 additions & 0 deletions src/assets/icons/library/svg/check-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/icons/library/svg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import BookIcon from './book.svg';
import BookShadowIcon from './book-shadow.svg';
import CalendarIcon from './calendar.svg';
import CheckIcon from './check.svg';
import CheckMarkIcon from './check-mark.svg';
import ChevronUpIcon from './chevron-up.svg';
import CloseIcon from './close.svg';
import CompanyIcon from './company.svg';
Expand Down Expand Up @@ -36,6 +37,7 @@ export {
BookShadowIcon,
CalendarIcon,
CheckIcon,
CheckMarkIcon,
ChevronUpIcon,
CloseIcon,
CompanyIcon,
Expand Down
4 changes: 2 additions & 2 deletions src/assets/icons/library/svg/share.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 18 additions & 10 deletions src/assets/icons/library/svg/video-shadow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const Header: FC = () => {
const { accountData, setAccountData } = useContext(GlobalContext);
const [{ toggleSidebar }, { isDarkTheme, isOpenedSidebar }] = useGlobals();

// Creating a library is gated by the `can-create-library` feature flag from
// GET /api/users/me (same flag the library page enforces). Drives whether the
// dropdown's "Create library" item is actionable.
const canCreateLibrary =
accountData?.featureNames?.includes('can-create-library') ?? false;

useEffect(() => {
const storedToken = localStorage.getItem('accessToken');
setToken(storedToken);
Expand Down Expand Up @@ -160,6 +166,7 @@ const Header: FC = () => {
setOpenLoginModal={setOpenLogin}
userImage={accountData?.picture}
handleOpenSettings={handleOpenSettings}
canCreateLibrary={canCreateLibrary}
hideDropdown={isOpenedSidebar}
hideUsername
/>
Expand Down Expand Up @@ -239,6 +246,7 @@ const Header: FC = () => {
setOpenLoginModal={setOpenLogin}
userImage={accountData?.picture}
handleOpenSettings={handleOpenSettings}
canCreateLibrary={canCreateLibrary}
/>
)}
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/components/UserProfile/UserProfile.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@
}
}

.menuItem.disabled {
color: #9e9e9e;
cursor: not-allowed;

.menuIcon {
opacity: 0.4;
}

&:hover {
background-color: transparent;
}
}

.menuIcon {
flex-shrink: 0;
}
Expand Down
44 changes: 42 additions & 2 deletions src/components/UserProfile/UserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Skeleton from 'react-loading-skeleton';
import { logout } from '@api/auth';

import LibraryIcon from '@icons/library/svg/library.svg';
import PlusIcon from '@icons/library/svg/plus.svg';

import 'react-loading-skeleton/dist/skeleton.css';
import styles from './UserProfile.module.scss';
Expand All @@ -18,16 +19,28 @@ type UserProfileProps = {
isDarkTheme?: boolean;
hideDropdown?: boolean;
hideUsername?: boolean;
canCreateLibrary?: boolean;
setAccountData?: (updater: (prev: boolean) => boolean) => void;
setOpenLoginModal?: (openModal: boolean) => void;
handleOpenSettings?: () => void;
};

const labels = {
en: { myLibrary: 'My Library', settings: 'Settings', logout: 'Log out' },
ru: { myLibrary: 'Моя библиотека', settings: 'Настройки', logout: 'Выйти' },
en: {
myLibrary: 'My Library',
createLibrary: 'Create library',
settings: 'Settings',
logout: 'Log out',
},
ru: {
myLibrary: 'Моя библиотека',
createLibrary: 'Создать библиотеку',
settings: 'Настройки',
logout: 'Выйти',
},
hy: {
myLibrary: 'Իմ գրադարանը',
createLibrary: 'Ստեղծել գրադարան',
settings: 'Կարգավորումներ',
logout: 'Դուրս գալ',
},
Expand All @@ -40,6 +53,7 @@ const UserProfile: FC<UserProfileProps> = ({
isDarkTheme,
hideDropdown,
hideUsername,
canCreateLibrary,
setAccountData,
setOpenLoginModal,
handleOpenSettings,
Expand Down Expand Up @@ -73,6 +87,16 @@ const UserProfile: FC<UserProfileProps> = ({
router.push(`/library/${username}`);
}, [router, username]);

// 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
// flag). So "Create library" just routes there; the flag drives whether the
// item is actionable at all.
const handleCreateLibrary = useCallback(() => {
if (!canCreateLibrary) return;
setIsDropdownOpen(false);
router.push(`/library/${username}`);
}, [router, username, canCreateLibrary]);

useEffect(() => {
if (hideDropdown) setIsDropdownOpen(false);
}, [hideDropdown]);
Expand Down Expand Up @@ -159,6 +183,22 @@ const UserProfile: FC<UserProfileProps> = ({
<span>{t.myLibrary}</span>
</div>
)}
<div
className={cn(styles.menuItem, {
[styles.disabled]: !canCreateLibrary,
})}
onClick={handleCreateLibrary}
aria-disabled={!canCreateLibrary}
>
<PlusIcon
width={14}
height={14}
className={cn(styles.menuIcon, {
[styles.menuIconDark]: isDarkTheme,
})}
/>
<span>{t.createLibrary}</span>
</div>
<div className={styles.menuItem} onClick={handleSettings}>
<Image
src="/keepsimple_/assets/icons/user-dropdown/settings.svg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export function AddShelfModal(props: AddShelfModalProps): JSX.Element {
type="text"
value={name}
onChange={e => handleNameChange(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddShelf();
}
}}
placeholder="My shelf"
placeholderColor="#9E9E9E"
ariaLabel="Shelf name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
// and a covered one keeps its spine and shadow. Spans the full 180×208 card.
.placeholder {
position: absolute;
bottom: -6px;
left: -5px;
bottom: -7px;
left: -3px;
width: 180px;
height: 208px;
z-index: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,20 @@
flex-direction: column;
color: #ffffff;
border-radius: 4px;
// Start fully transparent with no blur, then ramp both up on activation.
// Animating the blur *value* (not just the parent's opacity) forces the
// browser to composite the backdrop progressively, so the glass fades in
// smoothly instead of popping in once the reveal settles.
// Keep the glass blur on at all times so it's there the instant the card
// reveals rather than ramping in after; only the background wash and border
// fade on activation.
background: transparent;
border: 1px solid rgba(255, 255, 255, 0);
backdrop-filter: blur(0);
-webkit-backdrop-filter: blur(0);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
transition:
background 0.45s ease,
border-color 0.45s ease,
backdrop-filter 0.45s ease,
-webkit-backdrop-filter 0.45s ease;
border-color 0.45s ease;

&.active {
background: var(--info-card-bg);
border-color: #ffffff;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}

@media (max-width: 768px) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface LibraryInfoCardProps {
bookCount: number;
videoCount: number;
songCount: number;
/** Drives the glass background/blur fade-in so it ramps instead of popping. */
/** Fades the glass background wash and border in on reveal. */
isActive?: boolean;
className?: string;
}
18 changes: 18 additions & 0 deletions src/components/library/molecules/Modal/Modal.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,25 @@
}

.close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
flex-shrink: 0;

svg {
width: 16px;
height: 16px;

path {
fill: #aaaaaa;
}
}
}
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/components/library/molecules/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { CloseIcon } from '@icons/library/svg';

import { Text, TypographyVariant } from '@components/library/atoms/Text';

import { Button, ButtonSize, ButtonType } from '../Button';
import type { ModalProps } from './Modal.types';

import styles from './Modal.module.scss';
Expand Down Expand Up @@ -129,13 +128,14 @@ export function Modal(props: ModalProps): JSX.Element {
>
{title}
</Text>
<Button
<button
type="button"
className={styles.close}
aria-label="Close"
onClick={requestClose}
type={ButtonType.Text}
size={ButtonSize.Default}
ariaLabel="Close"
Icon={<CloseIcon />}
/>
>
<CloseIcon width={16} height={16} />
</button>
</div>
)}
{children}
Expand Down
Loading
Loading