sw-fe/src/components/Apps/ExtAppPostCard.jsx

183 lines
4.9 KiB
JavaScript

/**
* ExtAppPostCard - Render posts from external apps
*
* Supports two rendering modes:
* - Level 1: JSON config (uses CardRenderer)
* - Level 3: Pre-rendered HTML from sandbox (uses dangerouslySetInnerHTML)
*/
import React, { useMemo } from "react";
import CardRenderer from "../Cards/CardRenderer";
import { buildAssetUrl } from "../../api/extAppsApi";
// Default theme tokens for Level 1 cards
const DEFAULT_TOKENS = {
colors: {
surface: "#1a1a1a",
primary: "#8b5cf6",
text: "#ffffff",
textSecondary: "#a1a1aa",
accent: "#22c55e",
},
typography: {
title: { fontSize: 16, fontWeight: 600, color: "#ffffff" },
subtitle: { fontSize: 14, fontWeight: 400, color: "#a1a1aa" },
body: { fontSize: 13, color: "#d4d4d8" },
caption: { fontSize: 11, color: "#71717a" },
price: { fontSize: 18, fontWeight: 700, color: "#22c55e" },
},
chips: {
default: { background: "#27272a", color: "#d4d4d8", borderRadius: 12 },
primary: { background: "#8b5cf6", color: "#ffffff", borderRadius: 12 },
success: { background: "#166534", color: "#86efac", borderRadius: 12 },
},
buttons: {
primary: { background: "#8b5cf6", color: "#ffffff", borderRadius: 8 },
secondary: { background: "#27272a", color: "#d4d4d8", borderRadius: 8 },
},
radius: { card: 16 },
shadow: { card: "0 4px 12px rgba(0,0,0,0.3)" },
};
/**
* Process image URLs in post data to use asset proxy
*/
function processAssetUrls(post, appId) {
const processed = { ...post };
const processUrl = (url) => {
if (!url || typeof url !== "string") return url;
// If it's an app-relative path (not http)
if (!url.startsWith("http") && !url.startsWith("/api/assets")) {
return buildAssetUrl(appId, url, { w: 400 });
}
return url;
};
if (processed.image) processed.image = processUrl(processed.image);
if (processed.image_small) processed.image_small = processUrl(processed.image_small);
if (processed.image_medium) processed.image_medium = processUrl(processed.image_medium);
if (processed.thumbnail) processed.thumbnail = processUrl(processed.thumbnail);
return processed;
}
/**
* Default card spec for apps without custom card_style
*/
const DEFAULT_CARD_SPEC = {
size: { w: 280, h: 180 },
radius: 16,
background: { type: "image", bind: "data.image" },
layers: [
{
id: "gradient",
type: "rect",
x: 0,
y: 80,
w: 280,
h: 100,
style: "gradients.bottom",
},
{
id: "title",
type: "text",
x: 12,
y: 120,
w: 256,
bind: "data.title",
style: "typography.title",
maxLines: 2,
},
{
id: "source",
type: "text",
x: 12,
y: 155,
w: 200,
bind: "data.source_name",
style: "typography.caption",
},
],
};
export default function ExtAppPostCard({
post,
appId,
cardStyle,
onAction,
className = "",
}) {
// Process asset URLs
const processedPost = useMemo(
() => processAssetUrls(post, appId),
[post, appId]
);
// If post has pre-rendered content from Level 3 sandbox
if (post.rendered && post.rendered.html) {
return (
<div
className={`ext-app-card ext-app-card--rendered ${className}`}
style={{
width: post.rendered.width || 280,
height: post.rendered.height || 180,
borderRadius: 16,
overflow: "hidden",
boxShadow: DEFAULT_TOKENS.shadow.card,
}}
dangerouslySetInnerHTML={{ __html: post.rendered.html }}
onClick={() => onAction?.({ type: "open", bind: "post_id" }, post.post_id)}
/>
);
}
// Level 1: Use CardRenderer with JSON spec
const spec = cardStyle || DEFAULT_CARD_SPEC;
return (
<CardRenderer
spec={spec}
data={processedPost}
themeTokens={{
...DEFAULT_TOKENS,
gradients: {
bottom: "linear-gradient(to bottom, transparent, rgba(0,0,0,0.8))",
},
}}
onAction={(action, value) => onAction?.(action, value)}
className={`ext-app-card ${className}`}
/>
);
}
/**
* Small version of the card for map markers
*/
export function ExtAppPostMarker({ post, appId, onClick }) {
const imageUrl = post.image_small || post.thumbnail || post.image;
const processedUrl = imageUrl && !imageUrl.startsWith("http")
? buildAssetUrl(appId, imageUrl, { w: 80, h: 80 })
: imageUrl;
return (
<button
onClick={onClick}
className="w-12 h-12 rounded-full bg-zinc-800 border-2 border-purple-500 overflow-hidden shadow-lg hover:scale-110 transition-transform"
style={
processedUrl
? {
backgroundImage: `url(${processedUrl})`,
backgroundSize: "cover",
backgroundPosition: "center",
}
: {}
}
>
{!processedUrl && (
<i className="fas fa-map-marker-alt text-purple-400" />
)}
</button>
);
}