+ {shelves.length > 0 && (
+
+ )}
+ {isLoading ? (
+
+
+
+ ) : showNoCreatePermission ? (
+
+
+ You don't have permission to create a library
+
+
+ ) : shelves.length === 0 ? (
+
+
+ {viewAsOwner
+ ? 'Begin your journey by adding your first shelf'
+ : 'This library is empty'}
+
+
+ {viewAsOwner && (
+
+ )}
+
+ ) : normalizedSearch && displayedShelves.length === 0 ? (
+
+
+ Nothing matches “{search.trim()}”
+
+
+ ) : (
+
+ {displayedShelves.map(shelf => (
+
+ ))}
+
+ )}
+
+ {isOpen && (
+
s.attributes.name)}
+ />
+ )}
+
+ {viewAsOwner && (
+
+ )}
+
+ );
+}
diff --git a/src/layouts/library/Library/Library.types.ts b/src/layouts/library/Library/Library.types.ts
new file mode 100644
index 00000000..00541427
--- /dev/null
+++ b/src/layouts/library/Library/Library.types.ts
@@ -0,0 +1,3 @@
+export interface LibraryTemplateProps {
+ libraryId: string;
+}
diff --git a/src/layouts/library/Library/index.tsx b/src/layouts/library/Library/index.tsx
new file mode 100644
index 00000000..e03d3aae
--- /dev/null
+++ b/src/layouts/library/Library/index.tsx
@@ -0,0 +1,2 @@
+export * from './Library';
+export * from './Library.types';
diff --git a/src/lib/library/axios/index.ts b/src/lib/library/axios/index.ts
new file mode 100644
index 00000000..184c6313
--- /dev/null
+++ b/src/lib/library/axios/index.ts
@@ -0,0 +1,25 @@
+import axios, { InternalAxiosRequestConfig } from 'axios';
+
+import { getAccessToken } from '../cookie';
+
+const defaultOptions = {
+ baseURL: process.env.NEXT_PUBLIC_STRAPI,
+};
+
+const axiosInstance = axios.create(defaultOptions);
+
+axiosInstance.interceptors.request.use(
+ async (config: InternalAxiosRequestConfig) => {
+ const token = getAccessToken();
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+
+ return config;
+ },
+
+ error => Promise.reject(error),
+);
+
+export default axiosInstance;
diff --git a/src/lib/library/cookie/index.ts b/src/lib/library/cookie/index.ts
new file mode 100644
index 00000000..8d25e5ac
--- /dev/null
+++ b/src/lib/library/cookie/index.ts
@@ -0,0 +1,28 @@
+import Cookies from 'js-cookie';
+
+export const getCookie = (name: string) => {
+ return Cookies?.get(name) as string | number;
+};
+
+export const removeCookie = (name: string) => {
+ return Cookies.remove(name);
+};
+
+// Set default expires to 3 days if not provided
+export const setCookie = (name: string, value: string, expires: number = 3) => {
+ return Cookies.set(name, value, { expires });
+};
+
+// The login flow writes the JWT to BOTH localStorage and a session cookie, but
+// only the cookie is read by the library stack. The cookie has no `expires` (so
+// it dies with the browser session) and is `Secure` (dropped on http in some
+// browsers), whereas localStorage persists. Falling back to localStorage keeps
+// the library authenticated whenever the rest of the app still is.
+export const getAccessToken = (): string | undefined => {
+ const fromCookie = Cookies?.get('accessToken');
+ if (fromCookie) return fromCookie;
+ if (typeof window !== 'undefined') {
+ return window.localStorage.getItem('accessToken') ?? undefined;
+ }
+ return undefined;
+};
diff --git a/src/lib/library/mapSharedObject.ts b/src/lib/library/mapSharedObject.ts
new file mode 100644
index 00000000..522dde4b
--- /dev/null
+++ b/src/lib/library/mapSharedObject.ts
@@ -0,0 +1,72 @@
+import type { IMedia } from '@local-types/library/media';
+import type { IObject, IObjectAttributes } from '@local-types/library/object';
+import type { IStrapiRelation } from '@local-types/library/strapi';
+
+// The GET /api/share-links/:token payload's object shape isn't pinned yet:
+// Strapi may hand back the standard nested entity (`{ id, attributes }`) — the
+// same form `getSingleLibrary` returns — or a flattened record. Normalize either
+// into the nested IObject the card components read.
+// TODO(backend): pin the response shape and drop the flat-record fallback.
+export function mapSharedObject(raw: unknown): IObject | null {
+ if (!raw || typeof raw !== 'object') return null;
+ const record = raw as Record