Rebuild Inertia SSR artifacts after deferred artwork UI.

Keep bootstrap/ssr in sync with the client Vite build shipped in this stack.
This commit is contained in:
2026-08-29 12:25:50 +02:00
parent ab8fa6d845
commit 4c409ceb81
2 changed files with 341 additions and 83 deletions
+2
View File
@@ -2354,6 +2354,7 @@
"resources/js/components/profile/tabs/TabArtworks.jsx": [], "resources/js/components/profile/tabs/TabArtworks.jsx": [],
"resources/js/components/profile/tabs/TabCollections.jsx": [], "resources/js/components/profile/tabs/TabCollections.jsx": [],
"resources/js/components/profile/tabs/TabFavourites.jsx": [], "resources/js/components/profile/tabs/TabFavourites.jsx": [],
"resources/js/components/profile/tabs/TabFollowers.jsx": [],
"resources/js/components/profile/tabs/TabPosts.jsx": [], "resources/js/components/profile/tabs/TabPosts.jsx": [],
"resources/js/components/profile/tabs/TabStats.jsx": [], "resources/js/components/profile/tabs/TabStats.jsx": [],
"resources/js/components/profile/tabs/TabStories.jsx": [], "resources/js/components/profile/tabs/TabStories.jsx": [],
@@ -2471,6 +2472,7 @@
"resources/js/lib/uploadEndpoints.js": [], "resources/js/lib/uploadEndpoints.js": [],
"resources/js/lib/uploadNotices.js": [], "resources/js/lib/uploadNotices.js": [],
"resources/js/lib/uploadUtils.js": [], "resources/js/lib/uploadUtils.js": [],
"resources/js/lib/useDeferredSimilarAi.js": [],
"resources/js/lib/useNavContext.js": [], "resources/js/lib/useNavContext.js": [],
"resources/js/lib/worldAnalytics.js": [], "resources/js/lib/worldAnalytics.js": [],
"resources/js/ssr.jsx": [], "resources/js/ssr.jsx": [],
+339 -83
View File
@@ -41436,49 +41436,91 @@ function Skeleton$1() {
} }
function ArtworkComments({ function ArtworkComments({
artworkId, artworkId,
comments: initialComments = [],
isLoggedIn = false, isLoggedIn = false,
loginUrl = "/login" loginUrl = "/login"
}) { }) {
const [comments, setComments] = reactExports.useState(initialComments); const [comments, setComments2] = reactExports.useState([]);
const [loading, setLoading] = reactExports.useState(false); const [loading, setLoading] = reactExports.useState(false);
const [error, setError] = reactExports.useState(false);
const [page, setPage] = reactExports.useState(1); const [page, setPage] = reactExports.useState(1);
const [lastPage, setLastPage] = reactExports.useState(1); const [lastPage, setLastPage] = reactExports.useState(1);
const [total, setTotal] = reactExports.useState(initialComments.length); const [total, setTotal] = reactExports.useState(0);
const initialized = reactExports.useRef(false); const sectionRef = reactExports.useRef(null);
const requestRef = reactExports.useRef(null);
const generationRef = reactExports.useRef(0);
const loadComments = reactExports.useCallback( const loadComments = reactExports.useCallback(
async (p = 1) => { async (p = 1) => {
if (!artworkId) return; if (!artworkId || requestRef.current) return;
const generation = generationRef.current;
const controller = new AbortController();
requestRef.current = controller;
setLoading(true); setLoading(true);
setError(false);
try { try {
const { data } = await axios.get(`/api/artworks/${artworkId}/comments?page=${p}`); const { data } = await axios.get(`/api/artworks/${artworkId}/comments?page=${p}`, { signal: controller.signal });
if (generation !== generationRef.current) return;
if (p === 1) { if (p === 1) {
setComments(data.data ?? []); setComments2(data.data ?? []);
} else { } else {
setComments((prev) => [...prev, ...data.data ?? []]); setComments2((prev) => [...prev, ...data.data ?? []]);
} }
setPage(data.meta?.current_page ?? p); setPage(data.meta?.current_page ?? p);
setLastPage(data.meta?.last_page ?? 1); setLastPage(data.meta?.last_page ?? 1);
setTotal(data.meta?.total ?? 0); setTotal(data.meta?.total ?? 0);
} catch { } catch {
if (generation === generationRef.current && !controller.signal.aborted) setError(true);
} finally { } finally {
setLoading(false); if (requestRef.current === controller) {
requestRef.current = null;
if (generation === generationRef.current) setLoading(false);
}
} }
}, },
[artworkId] [artworkId]
); );
reactExports.useEffect(() => { reactExports.useEffect(() => {
if (initialized.current) return; generationRef.current += 1;
initialized.current = true; requestRef.current?.abort();
if (artworkId && initialComments.length === 0) { requestRef.current = null;
loadComments(1); setComments2([]);
} else { setLoading(false);
setTotal(initialComments.length); setError(false);
setPage(1);
setLastPage(1);
setTotal(0);
const section = sectionRef.current;
if (!section || !artworkId) return void 0;
let fallbackTimer = null;
let observer = null;
let established = false;
const trigger = () => {
observer?.disconnect();
if (!requestRef.current) loadComments(1);
};
if (typeof window !== "undefined" && typeof window.IntersectionObserver === "function") {
try {
observer = new window.IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) trigger();
}, { rootMargin: "1000px 0px" });
observer.observe(section);
established = true;
} catch {
observer?.disconnect();
} }
}, [artworkId, initialComments.length, loadComments]); }
if (!established && typeof window !== "undefined") {
fallbackTimer = window.setTimeout(trigger, 1200);
}
return () => {
observer?.disconnect();
if (fallbackTimer !== null && typeof window !== "undefined") window.clearTimeout(fallbackTimer);
requestRef.current?.abort();
requestRef.current = null;
};
}, [artworkId, loadComments]);
const handlePosted = reactExports.useCallback((newComment) => { const handlePosted = reactExports.useCallback((newComment) => {
const comment = { ...newComment, replies: newComment.replies || [] }; const comment = { ...newComment, replies: newComment.replies || [] };
setComments((prev) => [comment, ...prev]); setComments2((prev) => [comment, ...prev]);
setTotal((t) => t + 1); setTotal((t) => t + 1);
}, []); }, []);
const handleReplyPosted = reactExports.useCallback((parentId, newReply) => { const handleReplyPosted = reactExports.useCallback((parentId, newReply) => {
@@ -41491,10 +41533,10 @@ function ArtworkComments({
} }
return c; return c;
}); });
setComments((prev) => insertReply(prev)); setComments2((prev) => insertReply(prev));
setTotal((t) => t + 1); setTotal((t) => t + 1);
}, []); }, []);
return /* @__PURE__ */ React.createElement("section", { "aria-label": "Comments", className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold tracking-tight text-white sm:text-xl" }, "Comments"), total > 0 && /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full bg-white/[0.06] px-2.5 py-0.5 text-xs font-medium tabular-nums text-white/50" }, total)), loading && comments.length === 0 ? /* @__PURE__ */ React.createElement(Skeleton$1, null) : comments.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "flex flex-col items-center gap-3 rounded-2xl border border-dashed border-white/[0.08] bg-white/[0.015] px-6 py-10 text-center" }, /* @__PURE__ */ React.createElement(ChatBubbleIcon$1, null), /* @__PURE__ */ React.createElement("p", { className: "text-sm font-medium text-white/40" }, "No comments yet"), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-white/25" }, "Be the first to share your thoughts.")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("ul", { className: "space-y-3 sm:space-y-4" }, comments.map((comment) => /* @__PURE__ */ React.createElement( return /* @__PURE__ */ React.createElement("section", { ref: sectionRef, "aria-label": "Comments", className: "space-y-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React.createElement("h2", { className: "text-lg font-semibold tracking-tight text-white sm:text-xl" }, "Comments"), total > 0 && /* @__PURE__ */ React.createElement("span", { className: "inline-flex items-center rounded-full bg-white/[0.06] px-2.5 py-0.5 text-xs font-medium tabular-nums text-white/50" }, total)), loading && comments.length === 0 ? /* @__PURE__ */ React.createElement(Skeleton$1, null) : error && comments.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-dashed border-white/[0.08] bg-white/[0.015] px-6 py-8 text-center" }, /* @__PURE__ */ React.createElement("p", { className: "text-sm font-medium text-white/45" }, "Comments could not be loaded."), /* @__PURE__ */ React.createElement("button", { type: "button", onClick: () => loadComments(1), className: "mt-3 rounded-full border border-white/[0.1] px-4 py-2 text-xs font-medium text-white/65 hover:bg-white/[0.06]" }, "Try again")) : comments.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "flex flex-col items-center gap-3 rounded-2xl border border-dashed border-white/[0.08] bg-white/[0.015] px-6 py-10 text-center" }, /* @__PURE__ */ React.createElement(ChatBubbleIcon$1, null), /* @__PURE__ */ React.createElement("p", { className: "text-sm font-medium text-white/40" }, "No comments yet"), /* @__PURE__ */ React.createElement("p", { className: "text-xs text-white/25" }, "Be the first to share your thoughts.")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("ul", { className: "space-y-3 sm:space-y-4" }, comments.map((comment) => /* @__PURE__ */ React.createElement(
CommentItem$3, CommentItem$3,
{ {
key: comment.id, key: comment.id,
@@ -42906,6 +42948,20 @@ function ArtworkRecommendationsRails({ artwork, related = [], similarApiData = [
return /* @__PURE__ */ React.createElement("div", { className: "space-y-14" }, /* @__PURE__ */ React.createElement(Rail, { title: "Similar Artworks", emoji: "✨", items: similarItems, seeAllHref: similarHref }), /* @__PURE__ */ React.createElement(Rail, { title: trendingLabel, emoji: "🔥", items: trendingRailItems, seeAllHref: trendingHref })); return /* @__PURE__ */ React.createElement("div", { className: "space-y-14" }, /* @__PURE__ */ React.createElement(Rail, { title: "Similar Artworks", emoji: "✨", items: similarItems, seeAllHref: similarHref }), /* @__PURE__ */ React.createElement(Rail, { title: trendingLabel, emoji: "🔥", items: trendingRailItems, seeAllHref: trendingHref }));
} }
const fallbackCache = /* @__PURE__ */ new Map(); const fallbackCache = /* @__PURE__ */ new Map();
function normalizeNavigation(data) {
if (!data || typeof data !== "object") return null;
const hasPrev = data.prev_id == null || data.prev_url && data.prev_slug;
const hasNext = data.next_id == null || data.next_url && data.next_slug;
if (!hasPrev || !hasNext) return null;
return {
prevId: data.prev_id ?? null,
nextId: data.next_id ?? null,
prevUrl: data.prev_url ?? null,
nextUrl: data.next_url ?? null,
prevPreview: data.prev_preview ?? null,
nextPreview: data.next_preview ?? null
};
}
async function fetchFallback(artworkId) { async function fetchFallback(artworkId) {
const key = String(artworkId); const key = String(artworkId);
if (fallbackCache.has(key)) return fallbackCache.get(key); if (fallbackCache.has(key)) return fallbackCache.get(key);
@@ -42913,27 +42969,25 @@ async function fetchFallback(artworkId) {
const res = await fetch(`/api/artworks/navigation/${artworkId}`, { const res = await fetch(`/api/artworks/navigation/${artworkId}`, {
headers: { Accept: "application/json" } headers: { Accept: "application/json" }
}); });
if (!res.ok) return { prevId: null, nextId: null, prevUrl: null, nextUrl: null }; if (!res.ok) return { prevId: null, nextId: null, prevUrl: null, nextUrl: null, prevPreview: null, nextPreview: null };
const data = await res.json(); const data = await res.json();
const result = { const result = normalizeNavigation(data) || { prevId: null, nextId: null, prevUrl: null, nextUrl: null, prevPreview: null, nextPreview: null };
prevId: data.prev_id ?? null,
nextId: data.next_id ?? null,
prevUrl: data.prev_url ?? null,
nextUrl: data.next_url ?? null
};
fallbackCache.set(key, result); fallbackCache.set(key, result);
return result; return result;
} catch { } catch {
return { prevId: null, nextId: null, prevUrl: null, nextUrl: null }; return { prevId: null, nextId: null, prevUrl: null, nextUrl: null, prevPreview: null, nextPreview: null };
} }
} }
function useNavContext(currentArtworkId) { function useNavContext(currentArtworkId, embeddedNavigation = null) {
const getNeighbors = reactExports.useCallback(async () => { const getNeighbors = reactExports.useCallback(async () => {
const embedded = normalizeNavigation(embeddedNavigation);
if (embedded) return embedded;
return fetchFallback(currentArtworkId); return fetchFallback(currentArtworkId);
}, [currentArtworkId]); }, [currentArtworkId, embeddedNavigation]);
return { getNeighbors }; return { getNeighbors };
} }
const preloadCache = /* @__PURE__ */ new Set(); const preloadCache = /* @__PURE__ */ new Set();
const prefetchCache = /* @__PURE__ */ new Set();
function scheduleIdleTask(callback, delay = 1200) { function scheduleIdleTask(callback, delay = 1200) {
if (typeof window === "undefined") { if (typeof window === "undefined") {
callback(); callback();
@@ -42953,8 +43007,8 @@ function preloadImage(src2) {
const img = new Image(); const img = new Image();
img.src = src2; img.src = src2;
} }
function ArtworkNavigator({ artworkId, onNavigate, onOpenViewer, onReady }) { function ArtworkNavigator({ artworkId, navigation: navigation2 = null, onNavigate, onOpenViewer, onReady }) {
const { getNeighbors } = useNavContext(artworkId); const { getNeighbors } = useNavContext(artworkId, navigation2);
const [neighbors, setNeighbors] = reactExports.useState({ prevId: null, nextId: null, prevUrl: null, nextUrl: null }); const [neighbors, setNeighbors] = reactExports.useState({ prevId: null, nextId: null, prevUrl: null, nextUrl: null });
const navigatingRef = reactExports.useRef(false); const navigatingRef = reactExports.useRef(false);
const neighborsRef = reactExports.useRef(neighbors); const neighborsRef = reactExports.useRef(neighbors);
@@ -42989,22 +43043,45 @@ function ArtworkNavigator({ artworkId, onNavigate, onOpenViewer, onReady }) {
reactExports.useEffect(() => { reactExports.useEffect(() => {
const ids = [neighbors.prevId, neighbors.nextId].filter(Boolean); const ids = [neighbors.prevId, neighbors.nextId].filter(Boolean);
if (ids.length === 0) return void 0; if (ids.length === 0) return void 0;
const previews = /* @__PURE__ */ new Map([
[neighbors.prevId, neighbors.prevPreview],
[neighbors.nextId, neighbors.nextPreview]
]);
let cancelled = false; let cancelled = false;
const cancelIdleTask = scheduleIdleTask(() => { const cancelIdleTask = scheduleIdleTask(() => {
ids.forEach((id) => { ids.forEach((id) => {
fetch(`/api/artworks/${id}/page`, { headers: { Accept: "application/json" } }).then((r) => r.ok ? r.json() : null).then((data) => { if (prefetchCache.has(id)) return;
if (cancelled || !data) return; prefetchCache.add(id);
const preview = previews.get(id);
if (preview) {
if (!cancelled) {
const imgUrl = preview.thumbs?.lg?.url || preview.thumbs?.md?.url;
if (imgUrl) preloadImage(imgUrl);
}
return;
}
fetch(`/api/artworks/${id}/page?prefetch=1`, { headers: { Accept: "application/json" } }).then((r) => {
if (!r.ok) {
prefetchCache.delete(id);
return null;
}
return r.json();
}).then((data) => {
if (!data) {
prefetchCache.delete(id);
return;
}
if (cancelled) return;
const imgUrl = data.thumbs?.lg?.url || data.thumbs?.md?.url; const imgUrl = data.thumbs?.lg?.url || data.thumbs?.md?.url;
if (imgUrl) preloadImage(imgUrl); if (imgUrl) preloadImage(imgUrl);
}).catch(() => { }).catch(() => prefetchCache.delete(id));
});
}); });
}); });
return () => { return () => {
cancelled = true; cancelled = true;
cancelIdleTask(); cancelIdleTask();
}; };
}, [neighbors.prevId, neighbors.nextId]); }, [neighbors.prevId, neighbors.nextId, neighbors.prevPreview, neighbors.nextPreview]);
const navigate = reactExports.useCallback(async (targetId, targetUrl) => { const navigate = reactExports.useCallback(async (targetId, targetUrl) => {
if (!targetId && !targetUrl) return; if (!targetId && !targetUrl) return;
if (navigatingRef.current) return; if (navigatingRef.current) return;
@@ -43194,6 +43271,140 @@ function GroupSummaryPanel({ group, artwork }) {
if (!group) return null; if (!group) return null;
return /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[28px] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_36%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-5 py-5 shadow-[0_22px_55px_rgba(0,0,0,0.26)] backdrop-blur-xl sm:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-4" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]" }, group.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: group.avatar_url, alt: group.name, className: "h-full w-full object-cover", loading: "lazy" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-people-group text-slate-300" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Published as a group"), /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "mt-2 block text-xl font-semibold tracking-[-0.02em] text-white transition hover:text-sky-300" }, group.name), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-white/65" }, group.headline || group.bio_excerpt || `${artwork?.title || "This artwork"} is credited to a collaborative group identity on Skinbase.`))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, (Array.isArray(group.trust_signals) ? group.trust_signals : []).slice(0, 3).map((signal) => /* @__PURE__ */ React.createElement(GroupBadgePill, { key: signal.key, label: signal.label, tone: signal.tone })), group.is_recruiting ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Recruiting", tone: "emerald" }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-3 gap-2 rounded-2xl border border-white/10 bg-black/20 p-3 text-center" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.artworks || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Artworks")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.members || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Members")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.followers || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Followers"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, "Open group page"), group.urls?.releases ? /* @__PURE__ */ React.createElement("a", { href: group.urls.releases, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]" }, "Browse releases") : null)); return /* @__PURE__ */ React.createElement("section", { className: "overflow-hidden rounded-[28px] border border-white/[0.08] bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.14),transparent_36%),linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] px-5 py-5 shadow-[0_22px_55px_rgba(0,0,0,0.26)] backdrop-blur-xl sm:px-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-4" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]" }, group.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: group.avatar_url, alt: group.name, className: "h-full w-full object-cover", loading: "lazy" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-people-group text-slate-300" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/80" }, "Published as a group"), /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "mt-2 block text-xl font-semibold tracking-[-0.02em] text-white transition hover:text-sky-300" }, group.name), /* @__PURE__ */ React.createElement("p", { className: "mt-2 text-sm leading-6 text-white/65" }, group.headline || group.bio_excerpt || `${artwork?.title || "This artwork"} is credited to a collaborative group identity on Skinbase.`))), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, (Array.isArray(group.trust_signals) ? group.trust_signals : []).slice(0, 3).map((signal) => /* @__PURE__ */ React.createElement(GroupBadgePill, { key: signal.key, label: signal.label, tone: signal.tone })), group.is_recruiting ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Recruiting", tone: "emerald" }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid grid-cols-3 gap-2 rounded-2xl border border-white/10 bg-black/20 p-3 text-center" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.artworks || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Artworks")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.members || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Members")), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-lg font-semibold text-white" }, NUMBER_FORMATTER.format(Number(group.counts?.followers || 0))), /* @__PURE__ */ React.createElement("div", { className: "text-[11px] uppercase tracking-[0.16em] text-slate-500" }, "Followers"))), /* @__PURE__ */ React.createElement("div", { className: "mt-5 flex flex-wrap gap-2" }, /* @__PURE__ */ React.createElement("a", { href: group.urls?.public || "/groups", className: "rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15" }, "Open group page"), group.urls?.releases ? /* @__PURE__ */ React.createElement("a", { href: group.urls.releases, className: "rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:border-white/20 hover:bg-white/[0.07]" }, "Browse releases") : null));
} }
const REGISTRY = /* @__PURE__ */ new Map();
const ROOT_MARGIN = "800px 0px";
const IDLE_TIMEOUT = 3e3;
function scheduleIdle(callback) {
if (typeof window.requestIdleCallback === "function") {
const handle2 = window.requestIdleCallback(callback, { timeout: IDLE_TIMEOUT });
return () => window.cancelIdleCallback?.(handle2);
}
const handle = window.setTimeout(callback, IDLE_TIMEOUT);
return () => window.clearTimeout(handle);
}
function useDeferredSimilarAi(artworkId, anchorRef) {
const [state, setState] = reactExports.useState(() => ({
id: artworkId,
items: REGISTRY.get(Number(artworkId || 0))?.items || []
}));
const [loading, setLoading] = reactExports.useState(false);
const [error, setError] = reactExports.useState(false);
reactExports.useEffect(() => {
let cancelled = false;
const id = Number(artworkId || 0);
const controller = new AbortController();
let attachedEntry = null;
let triggered = false;
const currentEntry = REGISTRY.get(id);
const cached = currentEntry?.status === "completed" ? currentEntry.items : null;
setState({ id, items: cached || [] });
setLoading(false);
setError(false);
if (!id || cached) {
return () => controller.abort();
}
let stopIdle = () => {
};
let observer = null;
let observationEstablished = false;
const detach = () => {
if (!attachedEntry) return;
attachedEntry.subscribers -= 1;
const entry = attachedEntry;
attachedEntry = null;
if (entry.subscribers === 0 && entry.status === "in-flight") {
entry.controller.abort();
}
};
const attach = (entry) => {
if (cancelled || attachedEntry === entry) return;
attachedEntry = entry;
entry.subscribers += 1;
setLoading(true);
entry.promise.then((nextItems) => {
if (!cancelled) setState({ id, items: nextItems });
}).catch((requestError) => {
if (!cancelled && requestError?.name !== "AbortError") {
setState({ id, items: [] });
setError(true);
}
}).finally(() => {
if (!cancelled) setLoading(false);
if (attachedEntry === entry) {
attachedEntry = null;
entry.subscribers -= 1;
}
});
};
const load = async () => {
if (triggered || cancelled) return;
triggered = true;
observer?.disconnect();
stopIdle();
let entry = REGISTRY.get(id);
if (entry?.status === "completed") {
if (!cancelled) setState({ id, items: entry.items });
return;
}
if (entry?.status !== "in-flight" || entry.controller.signal.aborted) {
const requestController = new AbortController();
entry = {
status: "in-flight",
controller: requestController,
promise: null,
subscribers: 0
};
REGISTRY.set(id, entry);
entry.promise = Promise.resolve().then(() => fetch(`/api/art/${id}/similar-ai`, {
credentials: "same-origin",
signal: requestController.signal
})).then((response) => {
if (!response.ok) throw new Error("similar fetch failed");
return response.json();
}).then((payload) => {
const nextItems = Array.isArray(payload?.data) ? payload.data : [];
if (REGISTRY.get(id) === entry) {
REGISTRY.set(id, { status: "completed", items: nextItems });
}
return nextItems;
}).catch((requestError) => {
if (REGISTRY.get(id) === entry) REGISTRY.delete(id);
throw requestError;
});
entry.promise.catch(() => {
});
}
attach(entry);
};
if (typeof window.IntersectionObserver === "function" && anchorRef?.current) {
try {
observer = new window.IntersectionObserver((entries) => {
if (entries.some((entry) => entry.isIntersecting)) load();
}, { rootMargin: ROOT_MARGIN });
observer.observe(anchorRef.current);
observationEstablished = true;
} catch {
observer?.disconnect();
observer = null;
}
}
if (!observationEstablished) {
stopIdle = scheduleIdle(load);
}
return () => {
cancelled = true;
observer?.disconnect();
stopIdle();
detach();
controller.abort();
};
}, [artworkId, anchorRef]);
return {
items: Number(state.id || 0) === Number(artworkId || 0) ? state.items : [],
loading: Number(state.id || 0) === Number(artworkId || 0) ? loading : false,
error: Number(state.id || 0) === Number(artworkId || 0) ? error : false
};
}
function publisherToGroupSummary(publisher) { function publisherToGroupSummary(publisher) {
if (!publisher || publisher.type !== "group") return null; if (!publisher || publisher.type !== "group") return null;
return { return {
@@ -43215,7 +43426,7 @@ function publisherToGroupSummary(publisher) {
} }
}; };
} }
function ArtworkPage({ artwork: initialArtwork, related: initialRelated, presentMd: initialMd, presentLg: initialLg, presentXl: initialXl, presentSq: initialSq, canonicalUrl: initialCanonical, isAuthenticated = false, comments: initialComments = [], groupSummary: initialGroupSummary = null, reactionTotals: initialReactionTotals = {}, seo = null }) { function ArtworkPage({ artwork: initialArtwork, navigation: initialNavigation = null, related: initialRelated, presentMd: initialMd, presentLg: initialLg, presentXl: initialXl, presentSq: initialSq, canonicalUrl: initialCanonical, isAuthenticated = false, groupSummary: initialGroupSummary = null, reactionTotals: initialReactionTotals = {}, seo = null }) {
const [viewerOpen, setViewerOpen] = reactExports.useState(false); const [viewerOpen, setViewerOpen] = reactExports.useState(false);
const [showMatureArtwork, setShowMatureArtwork] = reactExports.useState(false); const [showMatureArtwork, setShowMatureArtwork] = reactExports.useState(false);
const openViewer = reactExports.useCallback(() => setViewerOpen(true), []); const openViewer = reactExports.useCallback(() => setViewerOpen(true), []);
@@ -43236,12 +43447,12 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
const [presentXl, setPresentXl] = reactExports.useState(initialXl); const [presentXl, setPresentXl] = reactExports.useState(initialXl);
const [presentSq, setPresentSq] = reactExports.useState(initialSq); const [presentSq, setPresentSq] = reactExports.useState(initialSq);
const [related, setRelated] = reactExports.useState(initialRelated); const [related, setRelated] = reactExports.useState(initialRelated);
const [comments, setComments] = reactExports.useState(initialComments);
const [canonicalUrl, setCanonicalUrl] = reactExports.useState(initialCanonical); const [canonicalUrl, setCanonicalUrl] = reactExports.useState(initialCanonical);
const [groupSummary, setGroupSummary] = reactExports.useState(initialGroupSummary || publisherToGroupSummary(initialArtwork?.publisher)); const [groupSummary, setGroupSummary] = reactExports.useState(initialGroupSummary || publisherToGroupSummary(initialArtwork?.publisher));
const [selectedMediaId, setSelectedMediaId] = reactExports.useState("cover"); const [selectedMediaId, setSelectedMediaId] = reactExports.useState("cover");
const [similarRecommendations, setSimilarRecommendations] = reactExports.useState([]);
const [trendingRecommendations, setTrendingRecommendations] = reactExports.useState([]); const [trendingRecommendations, setTrendingRecommendations] = reactExports.useState([]);
const similarAiAnchorRef = reactExports.useRef(null);
const { items: deferredSimilarRecommendations } = useDeferredSimilarAi(artwork?.id, similarAiAnchorRef);
const [navState, setNavState] = reactExports.useState({ hasPrev: false, hasNext: false, navigatePrev: null, navigateNext: null }); const [navState, setNavState] = reactExports.useState({ hasPrev: false, hasNext: false, navigatePrev: null, navigateNext: null });
const initialArtworkIdRef = reactExports.useRef(initialArtwork?.id); const initialArtworkIdRef = reactExports.useRef(initialArtwork?.id);
const [reactionTotals, setReactionTotals] = reactExports.useState(initialReactionTotals ?? {}); const [reactionTotals, setReactionTotals] = reactExports.useState(initialReactionTotals ?? {});
@@ -43250,27 +43461,6 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
if (artwork.id === initialArtworkIdRef.current) return; if (artwork.id === initialArtworkIdRef.current) return;
axios.get(`/api/artworks/${artwork.id}/reactions`).then(({ data }) => setReactionTotals(data.totals ?? {})).catch(() => setReactionTotals({})); axios.get(`/api/artworks/${artwork.id}/reactions`).then(({ data }) => setReactionTotals(data.totals ?? {})).catch(() => setReactionTotals({}));
}, [artwork?.id]); }, [artwork?.id]);
reactExports.useEffect(() => {
let isCancelled = false;
const loadSimilarRecommendations = async () => {
if (!artwork?.id) {
setSimilarRecommendations([]);
return;
}
try {
const response = await fetch(`/api/art/${artwork.id}/similar-ai`, { credentials: "same-origin" });
if (!response.ok) throw new Error("similar fetch failed");
const payload = await response.json();
if (!isCancelled) setSimilarRecommendations(payload?.data || []);
} catch {
if (!isCancelled) setSimilarRecommendations([]);
}
};
loadSimilarRecommendations();
return () => {
isCancelled = true;
};
}, [artwork?.id]);
reactExports.useEffect(() => { reactExports.useEffect(() => {
let isCancelled = false; let isCancelled = false;
const loadTrendingRecommendations = async () => { const loadTrendingRecommendations = async () => {
@@ -43308,7 +43498,6 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
setCanonicalUrl(data.canonical_url ?? window.location.href); setCanonicalUrl(data.canonical_url ?? window.location.href);
setGroupSummary(data.group_summary ?? publisherToGroupSummary(data.publisher)); setGroupSummary(data.group_summary ?? publisherToGroupSummary(data.publisher));
setSelectedMediaId("cover"); setSelectedMediaId("cover");
setSimilarRecommendations([]);
setTrendingRecommendations([]); setTrendingRecommendations([]);
setViewerOpen(false); setViewerOpen(false);
setShowMatureArtwork(false); setShowMatureArtwork(false);
@@ -43424,22 +43613,22 @@ function ArtworkPage({ artwork: initialArtwork, related: initialRelated, present
ArtworkComments, ArtworkComments,
{ {
artworkId: artwork.id, artworkId: artwork.id,
comments,
isLoggedIn: isAuthenticated, isLoggedIn: isAuthenticated,
loginUrl: "/login" loginUrl: "/login"
} }
)), /* @__PURE__ */ React.createElement("aside", { className: "space-y-5 lg:sticky lg:top-6 lg:self-start" }, /* @__PURE__ */ React.createElement(CreatorSpotlight, { artwork, presentSq, related }), groupSummary ? /* @__PURE__ */ React.createElement(GroupSummaryPanel, { group: groupSummary, artwork }) : null, /* @__PURE__ */ React.createElement(ArtworkDetailsPanel, { artwork, stats: liveStats }), /* @__PURE__ */ React.createElement(ArtworkAwards, { artwork, initialAwards, isAuthenticated })))), /* @__PURE__ */ React.createElement("div", { className: "mt-14 w-full max-w-screen-2xl mx-auto min-h-[640px]" }, /* @__PURE__ */ React.createElement( )), /* @__PURE__ */ React.createElement("aside", { className: "space-y-5 lg:sticky lg:top-6 lg:self-start" }, /* @__PURE__ */ React.createElement(CreatorSpotlight, { artwork, presentSq, related }), groupSummary ? /* @__PURE__ */ React.createElement(GroupSummaryPanel, { group: groupSummary, artwork }) : null, /* @__PURE__ */ React.createElement(ArtworkDetailsPanel, { artwork, stats: liveStats }), /* @__PURE__ */ React.createElement(ArtworkAwards, { artwork, initialAwards, isAuthenticated })))), /* @__PURE__ */ React.createElement("div", { ref: similarAiAnchorRef, className: "mt-14 w-full max-w-screen-2xl mx-auto min-h-[640px]" }, /* @__PURE__ */ React.createElement(
ArtworkRecommendationsRails, ArtworkRecommendationsRails,
{ {
artwork, artwork,
related, related,
similarApiData: similarRecommendations, similarApiData: deferredSimilarRecommendations,
trendingData: trendingRecommendations trendingData: trendingRecommendations
} }
))), /* @__PURE__ */ React.createElement( ))), /* @__PURE__ */ React.createElement(
ArtworkNavigator, ArtworkNavigator,
{ {
artworkId: artwork.id, artworkId: artwork.id,
navigation: artwork.navigation || initialNavigation,
onNavigate: handleNavigate, onNavigate: handleNavigate,
onOpenViewer: openViewer, onOpenViewer: openViewer,
onReady: setNavState onReady: setNavState
@@ -79881,7 +80070,7 @@ function CollectionShow() {
reportEndpoint reportEndpoint
} = props; } = props;
const [collection, setCollection] = React.useState(initialCollection); const [collection, setCollection] = React.useState(initialCollection);
const [comments, setComments] = React.useState(initialComments || []); const [comments, setComments2] = React.useState(initialComments || []);
const [submissions, setSubmissions] = React.useState(initialSubmissions || []); const [submissions, setSubmissions] = React.useState(initialSubmissions || []);
const [selectedArtworkId, setSelectedArtworkId] = React.useState(submissionArtworkOptions?.[0]?.id || ""); const [selectedArtworkId, setSelectedArtworkId] = React.useState(submissionArtworkOptions?.[0]?.id || "");
const [state, setState] = React.useState({ const [state, setState] = React.useState({
@@ -80148,12 +80337,12 @@ function CollectionShow() {
method: "POST", method: "POST",
body: { body: body2 } body: { body: body2 }
}); });
setComments(payload?.comments || []); setComments2(payload?.comments || []);
setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count })); setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count }));
} }
async function handleDeleteComment(commentId) { async function handleDeleteComment(commentId) {
const payload = await requestJson$l(`${commentsEndpoint}/${commentId}`, { method: "DELETE" }); const payload = await requestJson$l(`${commentsEndpoint}/${commentId}`, { method: "DELETE" });
setComments(payload?.comments || []); setComments2(payload?.comments || []);
setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count })); setCollection((current) => ({ ...current, comments_count: payload?.comments_count ?? current.comments_count }));
} }
async function handleSubmitArtwork() { async function handleSubmitArtwork() {
@@ -82717,7 +82906,7 @@ const FILTER_TABS = [
]; ];
function LatestCommentsPage({ initialComments = [], initialMeta = {}, isAuthenticated = false }) { function LatestCommentsPage({ initialComments = [], initialMeta = {}, isAuthenticated = false }) {
const [activeFilter, setActiveFilter] = reactExports.useState("all"); const [activeFilter, setActiveFilter] = reactExports.useState("all");
const [comments, setComments] = reactExports.useState(initialComments); const [comments, setComments2] = reactExports.useState(initialComments);
const [meta, setMeta] = reactExports.useState(initialMeta); const [meta, setMeta] = reactExports.useState(initialMeta);
const [loading, setLoading] = reactExports.useState(false); const [loading, setLoading] = reactExports.useState(false);
const [error, setError] = reactExports.useState(null); const [error, setError] = reactExports.useState(null);
@@ -82733,7 +82922,7 @@ function LatestCommentsPage({ initialComments = [], initialMeta = {}, isAuthenti
}); });
if (res.status === 401) { if (res.status === 401) {
setError("Please log in to view this feed."); setError("Please log in to view this feed.");
setComments([]); setComments2([]);
setMeta({}); setMeta({});
return; return;
} }
@@ -82742,7 +82931,7 @@ function LatestCommentsPage({ initialComments = [], initialMeta = {}, isAuthenti
return; return;
} }
const json = await res.json(); const json = await res.json();
setComments(json.data ?? []); setComments2(json.data ?? []);
setMeta(json.meta ?? {}); setMeta(json.meta ?? {});
} catch { } catch {
setError("Network error. Please try again."); setError("Network error. Please try again.");
@@ -83131,7 +83320,7 @@ function formatRelative$1(isoString) {
return `${d2}d ago`; return `${d2}d ago`;
} }
function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) { function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) {
const [comments, setComments] = reactExports.useState([]); const [comments, setComments2] = reactExports.useState([]);
const [loading, setLoading] = reactExports.useState(false); const [loading, setLoading] = reactExports.useState(false);
const [submitting, setSubmitting] = reactExports.useState(false); const [submitting, setSubmitting] = reactExports.useState(false);
const [body2, setBody] = reactExports.useState(""); const [body2, setBody] = reactExports.useState("");
@@ -83144,7 +83333,7 @@ function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) {
setLoading(true); setLoading(true);
try { try {
const { data } = await axios.get(`/api/posts/${postId}/comments`, { params: { page: p } }); const { data } = await axios.get(`/api/posts/${postId}/comments`, { params: { page: p } });
setComments((prev) => p === 1 ? data.data : [...prev, ...data.data]); setComments2((prev) => p === 1 ? data.data : [...prev, ...data.data]);
setHasMore(data.meta.current_page < data.meta.last_page); setHasMore(data.meta.current_page < data.meta.last_page);
setPage(p); setPage(p);
} catch { } catch {
@@ -83163,7 +83352,7 @@ function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) {
setError(null); setError(null);
try { try {
const { data } = await axios.post(`/api/posts/${postId}/comments`, { body: body2 }); const { data } = await axios.post(`/api/posts/${postId}/comments`, { body: body2 });
setComments((prev) => [...prev, data.comment]); setComments2((prev) => [...prev, data.comment]);
setBody(""); setBody("");
} catch (err) { } catch (err) {
setError(err.response?.data?.message ?? "Failed to post comment."); setError(err.response?.data?.message ?? "Failed to post comment.");
@@ -83175,7 +83364,7 @@ function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) {
if (!window.confirm("Delete this comment?")) return; if (!window.confirm("Delete this comment?")) return;
try { try {
await axios.delete(`/api/posts/${postId}/comments/${commentId}`); await axios.delete(`/api/posts/${postId}/comments/${commentId}`);
setComments((prev) => prev.filter((c) => c.id !== commentId)); setComments2((prev) => prev.filter((c) => c.id !== commentId));
} catch { } catch {
} }
}; };
@@ -83183,12 +83372,12 @@ function PostComments({ postId, isLoggedIn, isOwn = false, initialCount = 0 }) {
try { try {
if (comment.is_highlighted) { if (comment.is_highlighted) {
await axios.delete(`/api/posts/${postId}/comments/${comment.id}/highlight`); await axios.delete(`/api/posts/${postId}/comments/${comment.id}/highlight`);
setComments( setComments2(
(prev) => prev.map((c) => c.id === comment.id ? { ...c, is_highlighted: false } : c) (prev) => prev.map((c) => c.id === comment.id ? { ...c, is_highlighted: false } : c)
); );
} else { } else {
await axios.post(`/api/posts/${postId}/comments/${comment.id}/highlight`); await axios.post(`/api/posts/${postId}/comments/${comment.id}/highlight`);
setComments( setComments2(
(prev) => prev.map((c) => ({ ...c, is_highlighted: c.id === comment.id })) (prev) => prev.map((c) => ({ ...c, is_highlighted: c.id === comment.id }))
); );
} }
@@ -94339,7 +94528,7 @@ function Skeleton() {
))); )));
} }
function NewsComments({ articleId, isLoggedIn = false, loginUrl = "/login" }) { function NewsComments({ articleId, isLoggedIn = false, loginUrl = "/login" }) {
const [comments, setComments] = reactExports.useState([]); const [comments, setComments2] = reactExports.useState([]);
const [loading, setLoading] = reactExports.useState(false); const [loading, setLoading] = reactExports.useState(false);
const [page, setPage] = reactExports.useState(1); const [page, setPage] = reactExports.useState(1);
const [lastPage, setLastPage] = reactExports.useState(1); const [lastPage, setLastPage] = reactExports.useState(1);
@@ -94352,9 +94541,9 @@ function NewsComments({ articleId, isLoggedIn = false, loginUrl = "/login" }) {
try { try {
const { data } = await axios.get(`/api/news/articles/${articleId}/comments?page=${nextPage}`); const { data } = await axios.get(`/api/news/articles/${articleId}/comments?page=${nextPage}`);
if (nextPage === 1) { if (nextPage === 1) {
setComments(data.data ?? []); setComments2(data.data ?? []);
} else { } else {
setComments((current) => [...current, ...data.data ?? []]); setComments2((current) => [...current, ...data.data ?? []]);
} }
setPage(data.meta?.current_page ?? nextPage); setPage(data.meta?.current_page ?? nextPage);
setLastPage(data.meta?.last_page ?? 1); setLastPage(data.meta?.last_page ?? 1);
@@ -94371,7 +94560,7 @@ function NewsComments({ articleId, isLoggedIn = false, loginUrl = "/login" }) {
}, [loadComments]); }, [loadComments]);
const handlePosted = reactExports.useCallback((newComment) => { const handlePosted = reactExports.useCallback((newComment) => {
const comment = { ...newComment, replies: newComment.replies || [] }; const comment = { ...newComment, replies: newComment.replies || [] };
setComments((current) => [comment, ...current]); setComments2((current) => [comment, ...current]);
setTotal((current) => current + 1); setTotal((current) => current + 1);
}, []); }, []);
const handleReplyPosted = reactExports.useCallback((parentId, newReply) => { const handleReplyPosted = reactExports.useCallback((parentId, newReply) => {
@@ -94384,7 +94573,7 @@ function NewsComments({ articleId, isLoggedIn = false, loginUrl = "/login" }) {
} }
return comment; return comment;
}); });
setComments((current) => insertReply(current)); setComments2((current) => insertReply(current));
setTotal((current) => current + 1); setTotal((current) => current + 1);
}, []); }, []);
const handleDelete = reactExports.useCallback(async (commentId) => { const handleDelete = reactExports.useCallback(async (commentId) => {
@@ -95510,7 +95699,8 @@ const TABS$1 = [
{ id: "about", label: "About", icon: "fa-id-card" }, { id: "about", label: "About", icon: "fa-id-card" },
{ id: "stats", label: "Stats", icon: "fa-chart-bar" }, { id: "stats", label: "Stats", icon: "fa-chart-bar" },
{ id: "favourites", label: "Favourites", icon: "fa-heart" }, { id: "favourites", label: "Favourites", icon: "fa-heart" },
{ id: "activity", label: "Activity", icon: "fa-bolt" } { id: "activity", label: "Activity", icon: "fa-bolt" },
{ id: "followers", label: "Followers", icon: "fa-users" }
]; ];
function ProfileTabs({ activeTab, onTabChange }) { function ProfileTabs({ activeTab, onTabChange }) {
const navRef = reactExports.useRef(null); const navRef = reactExports.useRef(null);
@@ -98120,12 +98310,65 @@ function TabWorlds({ worldHistory, isOwner }) {
/* @__PURE__ */ React.createElement(ProfileWorldHistorySection, { history: worldHistory, isOwner }) /* @__PURE__ */ React.createElement(ProfileWorldHistorySection, { history: worldHistory, isOwner })
); );
} }
function formatFollowedAt(value) {
if (!value) return null;
try {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return date.toLocaleDateString(void 0, {
year: "numeric",
month: "short",
day: "numeric"
});
} catch {
return null;
}
}
function TabFollowers({ listing, username, followerCount }) {
const items = Array.isArray(listing?.data) ? listing.data : [];
const total = Number(listing?.total ?? followerCount ?? 0);
const currentPage = Number(listing?.current_page ?? 1);
const lastPage = Number(listing?.last_page ?? 1);
const prevUrl = listing?.prev_page_url ?? null;
const nextUrl = listing?.next_page_url ?? null;
return /* @__PURE__ */ React.createElement(
"div",
{
id: "tabpanel-followers",
role: "tabpanel",
"aria-labelledby": "tab-followers",
className: "pt-6"
},
/* @__PURE__ */ React.createElement("h2", { className: "mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-slate-500" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-users text-sky-400 fa-fw" }), "Followers", /* @__PURE__ */ React.createElement("span", { className: "rounded bg-white/5 px-1.5 py-0.5 text-[10px] font-medium normal-case tracking-normal text-slate-400" }, total.toLocaleString())),
items.length === 0 ? /* @__PURE__ */ React.createElement("p", { className: "rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-8 text-center text-sm text-slate-400" }, username ? `${username} has no public followers yet.` : "No public followers yet.") : /* @__PURE__ */ React.createElement("ul", { className: "grid gap-3 sm:grid-cols-2" }, items.map((follower) => {
const handle = follower.username || follower.display_name || follower.name;
const followedAt = formatFollowedAt(follower.followed_at);
return /* @__PURE__ */ React.createElement("li", { key: follower.id }, /* @__PURE__ */ React.createElement(
"a",
{
href: follower.profile_url,
className: "flex items-center gap-3 rounded-2xl border border-white/8 bg-white/[0.03] px-3 py-3 transition-colors hover:border-sky-300/25 hover:bg-white/[0.06]"
},
follower.avatar_url ? /* @__PURE__ */ React.createElement(
"img",
{
src: follower.avatar_url,
alt: "",
className: "h-12 w-12 rounded-xl object-cover"
}
) : /* @__PURE__ */ React.createElement("span", { className: "flex h-12 w-12 items-center justify-center rounded-xl bg-white/5 text-slate-500" }, /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-user" })),
/* @__PURE__ */ React.createElement("span", { className: "min-w-0" }, /* @__PURE__ */ React.createElement("span", { className: "block truncate text-sm font-semibold text-white" }, handle), follower.username ? /* @__PURE__ */ React.createElement("span", { className: "block truncate text-xs text-slate-400" }, "@", follower.username) : null, /* @__PURE__ */ React.createElement("span", { className: "mt-1 block text-[11px] text-slate-500" }, [follower.rank, followedAt ? `Followed ${followedAt}` : null].filter(Boolean).join(" · ")))
));
})),
lastPage > 1 ? /* @__PURE__ */ React.createElement("nav", { className: "mt-6 flex items-center justify-center gap-3 text-sm", "aria-label": "Followers pagination" }, prevUrl ? /* @__PURE__ */ React.createElement("a", { href: prevUrl, className: "rounded-xl border border-white/10 px-3 py-2 text-slate-300 hover:bg-white/5" }, "Previous") : /* @__PURE__ */ React.createElement("span", { className: "rounded-xl border border-white/5 px-3 py-2 text-slate-600" }, "Previous"), /* @__PURE__ */ React.createElement("span", { className: "text-slate-500" }, "Page ", currentPage, " of ", lastPage), nextUrl ? /* @__PURE__ */ React.createElement("a", { href: nextUrl, className: "rounded-xl border border-white/10 px-3 py-2 text-slate-300 hover:bg-white/5" }, "Next") : /* @__PURE__ */ React.createElement("span", { className: "rounded-xl border border-white/5 px-3 py-2 text-slate-600" }, "Next")) : null
);
}
function GroupProfileSummary({ contributions = [], href = null }) { function GroupProfileSummary({ contributions = [], href = null }) {
if (!Array.isArray(contributions) || contributions.length === 0) return null; if (!Array.isArray(contributions) || contributions.length === 0) return null;
const featured = contributions.slice(0, 3); const featured = contributions.slice(0, 3);
return /* @__PURE__ */ React.createElement("section", { className: "mx-auto mt-8 max-w-6xl px-4 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(14,165,233,0.12),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_18px_55px_rgba(2,6,23,0.28)] backdrop-blur-xl sm:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Group footprint"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Collaborative work across public groups"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "See the groups this creator contributes to through releases, credited artworks, and shared publishing activity.")), href ? /* @__PURE__ */ React.createElement("a", { href, className: "text-sm font-semibold text-sky-200 transition hover:text-white" }, "View full contribution history") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 lg:grid-cols-3" }, featured.map((entry) => /* @__PURE__ */ React.createElement("a", { key: entry.group?.slug || entry.group?.id, href: entry.group?.profile_url || "/groups", className: "rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20 hover:bg-black/30" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]" }, entry.group?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: entry.group.avatar_url, alt: entry.group?.name, className: "h-full w-full object-cover", loading: "lazy" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-people-group text-slate-300" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "truncate text-base font-semibold text-white" }, entry.group?.name), entry.role?.label ? /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, entry.role.label) : null, entry.summary ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-300" }, entry.summary) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, entry.trusted ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Trusted contributor", tone: "sky" }) : null, entry.recent_release_titles?.length ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Recent releases", tone: "amber" }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-4 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.artworks || 0).toLocaleString("en-US"), " artworks"), /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.releases || 0).toLocaleString("en-US"), " releases"), /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.projects || 0).toLocaleString("en-US"), " projects"))))))); return /* @__PURE__ */ React.createElement("section", { className: "mx-auto mt-8 max-w-6xl px-4 sm:px-6 lg:px-8" }, /* @__PURE__ */ React.createElement("div", { className: "rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,rgba(14,165,233,0.12),transparent_34%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))] p-5 shadow-[0_18px_55px_rgba(2,6,23,0.28)] backdrop-blur-xl sm:p-6" }, /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "text-[11px] font-semibold uppercase tracking-[0.24em] text-sky-200/80" }, "Group footprint"), /* @__PURE__ */ React.createElement("h2", { className: "mt-2 text-2xl font-semibold text-white" }, "Collaborative work across public groups"), /* @__PURE__ */ React.createElement("p", { className: "mt-2 max-w-3xl text-sm leading-6 text-slate-400" }, "See the groups this creator contributes to through releases, credited artworks, and shared publishing activity.")), href ? /* @__PURE__ */ React.createElement("a", { href, className: "text-sm font-semibold text-sky-200 transition hover:text-white" }, "View full contribution history") : null), /* @__PURE__ */ React.createElement("div", { className: "mt-5 grid gap-4 lg:grid-cols-3" }, featured.map((entry) => /* @__PURE__ */ React.createElement("a", { key: entry.group?.slug || entry.group?.id, href: entry.group?.profile_url || "/groups", className: "rounded-[24px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20 hover:bg-black/30" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-2xl border border-white/10 bg-white/[0.04]" }, entry.group?.avatar_url ? /* @__PURE__ */ React.createElement("img", { src: entry.group.avatar_url, alt: entry.group?.name, className: "h-full w-full object-cover", loading: "lazy" }) : /* @__PURE__ */ React.createElement("i", { className: "fa-solid fa-people-group text-slate-300" })), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "truncate text-base font-semibold text-white" }, entry.group?.name), entry.role?.label ? /* @__PURE__ */ React.createElement("div", { className: "mt-1 text-sm text-slate-400" }, entry.role.label) : null, entry.summary ? /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-sm text-slate-300" }, entry.summary) : null)), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-2" }, entry.trusted ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Trusted contributor", tone: "sky" }) : null, entry.recent_release_titles?.length ? /* @__PURE__ */ React.createElement(GroupBadgePill, { label: "Recent releases", tone: "amber" }) : null), /* @__PURE__ */ React.createElement("div", { className: "mt-4 flex flex-wrap gap-4 text-xs text-slate-400" }, /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.artworks || 0).toLocaleString("en-US"), " artworks"), /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.releases || 0).toLocaleString("en-US"), " releases"), /* @__PURE__ */ React.createElement("span", null, Number(entry.counts?.projects || 0).toLocaleString("en-US"), " projects")))))));
} }
const VALID_TABS = ["posts", "artworks", "stories", "achievements", "worlds", "collections", "about", "stats", "favourites", "activity"]; const VALID_TABS = ["posts", "artworks", "stories", "achievements", "worlds", "collections", "about", "stats", "favourites", "activity", "followers"];
function getInitialTab(initialTab = "posts") { function getInitialTab(initialTab = "posts") {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return VALID_TABS.includes(initialTab) ? initialTab : "posts"; return VALID_TABS.includes(initialTab) ? initialTab : "posts";
@@ -98154,6 +98397,7 @@ function ProfileShow() {
socialLinks, socialLinks,
followerCount, followerCount,
recentFollowers, recentFollowers,
followersListing,
followContext, followContext,
followAnalytics, followAnalytics,
suggestedUsers, suggestedUsers,
@@ -98184,6 +98428,11 @@ function ProfileShow() {
const [activeTab, setActiveTab] = reactExports.useState(() => getInitialTab(initialTab)); const [activeTab, setActiveTab] = reactExports.useState(() => getInitialTab(initialTab));
const handleTabChange = reactExports.useCallback((tab2) => { const handleTabChange = reactExports.useCallback((tab2) => {
if (!VALID_TABS.includes(tab2)) return; if (!VALID_TABS.includes(tab2)) return;
if (tab2 === "followers") {
const targetBase = profileTabUrls?.followers || `${profileUrl || ""}/followers`;
window.location.assign(targetBase);
return;
}
setActiveTab(tab2); setActiveTab(tab2);
try { try {
const currentUrl = new URL(window.location.href); const currentUrl = new URL(window.location.href);
@@ -98344,6 +98593,13 @@ function ProfileShow() {
followerCount, followerCount,
creatorStories creatorStories
} }
), activeTab === "followers" && /* @__PURE__ */ React.createElement(
TabFollowers,
{
listing: followersListing,
username: user.username || user.name,
followerCount
}
)))); ))));
} }
const __vite_glob_0_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ const __vite_glob_0_110 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({