From c2802e1da0bdbc06f4f58fe6638044d553747c62 Mon Sep 17 00:00:00 2001 From: MaryWylde Date: Thu, 18 Jun 2026 11:33:12 +0200 Subject: [PATCH] library: fix avatar quality, share-link object modals, share copy, and owner-named libraries - Avatar serves a correctly-sized image via next/image fill + sizes instead of upscaling a 48px source - Shared-link recipient view opens the object overview modal on card click - Object overview Share button copies the URL and shows "Copied" - Library cards/headers use "'s library" instead of "Library N" Co-Authored-By: Claude Opus 4.7 --- .../library/atoms/Avatar/Avatar.module.scss | 2 ++ .../library/atoms/Avatar/Avatar.tsx | 7 +++- .../ObjectOverviewModal.tsx | 35 +++++++++++++++---- .../ShareSelectionPanel.tsx | 29 ++++++++++----- .../ShareSelectionPanel.types.ts | 3 ++ .../library/[username]/share/[token].tsx | 13 +++++++ src/utils/library/mapStrapiLibraries.ts | 12 ++++--- 7 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/components/library/atoms/Avatar/Avatar.module.scss b/src/components/library/atoms/Avatar/Avatar.module.scss index 8f5fb435..0adedc85 100644 --- a/src/components/library/atoms/Avatar/Avatar.module.scss +++ b/src/components/library/atoms/Avatar/Avatar.module.scss @@ -1,4 +1,5 @@ .avatar { + position: relative; width: 100%; height: 100%; overflow: hidden; @@ -8,6 +9,7 @@ img { width: 100%; height: 100%; + object-fit: cover; } } diff --git a/src/components/library/atoms/Avatar/Avatar.tsx b/src/components/library/atoms/Avatar/Avatar.tsx index 7887c24e..d9915d54 100644 --- a/src/components/library/atoms/Avatar/Avatar.tsx +++ b/src/components/library/atoms/Avatar/Avatar.tsx @@ -14,7 +14,12 @@ export function Avatar(props: AvatarProps): JSX.Element { return (
{url ? ( - Picture of the author + Picture of the author ) : ( )} diff --git a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx index 0d4bfe07..d1945e17 100644 --- a/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx +++ b/src/components/library/organisms/ObjectOverviewModal/ObjectOverviewModal.tsx @@ -2,7 +2,7 @@ import { resolveStrapiUrl } from '@utils/library/resolveStrapiUrl'; import classNames from 'classnames'; import React, { JSX, useCallback, useMemo, useState } from 'react'; -import { SHELF_FULL_MESSAGE } from '@constants/library/common'; +import { KEEPSIMPLE_URL, SHELF_FULL_MESSAGE } from '@constants/library/common'; import type { Difficulty, @@ -12,6 +12,7 @@ import type { import { useClickOutside } from '@hooks/library/useClickOutside'; +import { objectSlug } from '@lib/library/objectSlug'; import { isShelfFullError } from '@lib/library/shelfFull'; import { sanitizeHtml } from '@lib/sanitizeHtml'; @@ -51,6 +52,10 @@ import type { ObjectOverviewModalProps } from './ObjectOverviewModal.types'; import styles from './ObjectOverviewModal.module.scss'; +// Canonical public host for shareable links — env-driven in staging/prod, +// falling back to the production domain (mirrors ShareSelectionPanel). +const SHARE_BASE_URL = process.env.NEXT_PUBLIC_DOMAIN ?? KEEPSIMPLE_URL; + function formatDate(iso?: string): string | null { if (!iso) return null; const d = new Date(iso); @@ -112,6 +117,8 @@ export function ObjectOverviewModal( const [moveLoading, setMoveLoading] = useState(false); const [moveError, setMoveError] = useState(null); + const [shareCopied, setShareCopied] = useState(false); + const { currentShelves } = useGlobalState(); const closeMenu = useCallback(() => setMenuOpen(false), []); @@ -154,10 +161,24 @@ export function ObjectOverviewModal( } }; - const handleShare = () => { - // TODO: Implement Share when sharing route + tokens exist. - // Likely: copy a public URL to clipboard like /share/objects/. - // Depends on a public-read endpoint or signed URL from backend. + const handleShare = async () => { + const url = `${SHARE_BASE_URL}/library/${ownerUsername}/${objectSlug(object)}`; + try { + await navigator.clipboard.writeText(url); + } catch { + // Clipboard API is unavailable on insecure origins / older browsers — + // fall back to a throwaway textarea + execCommand so copy still works. + const textarea = document.createElement('textarea'); + textarea.value = url; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + setShareCopied(true); + setTimeout(() => setShareCopied(false), 2000); }; // Fall back to `defaultShelfId` (the shelf this object is rendered under): @@ -347,8 +368,8 @@ export function ObjectOverviewModal( )} - {/* Drag handle wraps the card; the card carries no onClick here, so a - pointer-press always starts a drag instead of opening the overview. */} + {/* Owner view: the handle wraps the card and the card carries no onClick, + so a pointer-press starts a drag instead of opening the overview. + Recipient view: no drag, so the card click opens the overview modal. */}
- +
{!readOnly && sequence} @@ -124,6 +135,7 @@ export function ShareSelectionPanel({ onReorder, onRemove, onClear, + onObjectClick, className, }: ShareSelectionPanelProps): JSX.Element | null { const [collapsed, setCollapsed] = useState(false); @@ -274,6 +286,7 @@ export function ShareSelectionPanel({ position={index + 1} readOnly={readOnly} onRemove={setPendingRemoveId} + onObjectClick={onObjectClick} /> ))}
diff --git a/src/components/library/organisms/ShareSelectionPanel/ShareSelectionPanel.types.ts b/src/components/library/organisms/ShareSelectionPanel/ShareSelectionPanel.types.ts index d7f1c773..f3b797cc 100644 --- a/src/components/library/organisms/ShareSelectionPanel/ShareSelectionPanel.types.ts +++ b/src/components/library/organisms/ShareSelectionPanel/ShareSelectionPanel.types.ts @@ -12,5 +12,8 @@ export interface ShareSelectionPanelProps { onReorder?: (next: IObject[]) => void; onRemove?: (id: number) => void; onClear?: () => void; + // Recipient view only: open the object's overview modal on card click, + // mirroring how a shelf card opens it. + onObjectClick?: (object: IObject) => void; className?: string; } diff --git a/src/pages/library/[username]/share/[token].tsx b/src/pages/library/[username]/share/[token].tsx index e0400104..d50aa323 100644 --- a/src/pages/library/[username]/share/[token].tsx +++ b/src/pages/library/[username]/share/[token].tsx @@ -4,6 +4,7 @@ import React, { JSX, useEffect, useState } from 'react'; import { isLibraryEnabled } from '@constants/library/common'; import { DEFAULT_SEO } from '@constants/library/seo.config'; +import type { IObject } from '@local-types/library/object'; import type { IShareLinkView, ShareLinkStatus, @@ -23,6 +24,7 @@ import { } from '@components/library/molecules/Button'; import { Modal, useModalClose } from '@components/library/molecules/Modal'; import { SharedWithYouModal } from '@components/library/molecules/SharedWithYouModal'; +import { ObjectOverviewModal } from '@components/library/organisms/ObjectOverviewModal'; import { ShareSelectionPanel } from '@components/library/organisms/ShareSelectionPanel'; import { Sidebar } from '@components/library/organisms/Sidebar'; import SeoGenerator from '@components/SeoGenerator'; @@ -104,6 +106,7 @@ function ShareRecipientView({ username, token }: SharePageProps): JSX.Element { const [introOpen, setIntroOpen] = useState(false); const [panelOpen, setPanelOpen] = useState(false); const [errorDismissed, setErrorDismissed] = useState(false); + const [activeObject, setActiveObject] = useState(null); useEffect(() => { let active = true; @@ -144,6 +147,16 @@ function ShareRecipientView({ username, token }: SharePageProps): JSX.Element { objects={view.objects} ownerUsername={username} readOnly + onObjectClick={setActiveObject} + /> + )} + + {activeObject && ( + setActiveObject(null)} /> )} diff --git a/src/utils/library/mapStrapiLibraries.ts b/src/utils/library/mapStrapiLibraries.ts index d9f3ec0f..9b16af3c 100644 --- a/src/utils/library/mapStrapiLibraries.ts +++ b/src/utils/library/mapStrapiLibraries.ts @@ -67,15 +67,19 @@ export function mapStrapiLibraryEntryToCard( ); const aboutMePlain = stripHtml(attributes.aboutMe); - const libraryName = - attributes.name?.trim() || aboutLibraryPlain || `Library ${id}`; + const username = attributes.user?.data?.attributes?.username; + + // Libraries are owner-scoped, so present them as "'s library" rather + // than the raw `name`/id. Fall back to the explicit name, then about text, + // then the id only when there's no linked username to build the label from. + const libraryName = username + ? `${username}'s library` + : attributes.name?.trim() || aboutLibraryPlain || `Library ${id}`; const description = aboutMePlain || aboutLibraryPlain; const avatarUrl = attributes.avatar?.data?.attributes?.url; - const username = attributes.user?.data?.attributes?.username; - return { id, username,