@cedros/data-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +177 -0
- package/dist/admin/api.d.ts +19 -0
- package/dist/admin/api.js +108 -0
- package/dist/admin/components.d.ts +36 -0
- package/dist/admin/components.js +22 -0
- package/dist/admin/history.d.ts +17 -0
- package/dist/admin/history.js +103 -0
- package/dist/admin/icons.d.ts +15 -0
- package/dist/admin/icons.js +18 -0
- package/dist/admin/index.d.ts +13 -0
- package/dist/admin/index.js +12 -0
- package/dist/admin/permissions.d.ts +4 -0
- package/dist/admin/permissions.js +45 -0
- package/dist/admin/plugin.d.ts +4 -0
- package/dist/admin/plugin.js +180 -0
- package/dist/admin/primitives/ConfirmDialog.d.ts +14 -0
- package/dist/admin/primitives/ConfirmDialog.js +7 -0
- package/dist/admin/primitives/DataTable.d.ts +14 -0
- package/dist/admin/primitives/DataTable.js +7 -0
- package/dist/admin/primitives/DiffViewer.d.ts +11 -0
- package/dist/admin/primitives/DiffViewer.js +8 -0
- package/dist/admin/primitives/FormFieldRow.d.ts +23 -0
- package/dist/admin/primitives/FormFieldRow.js +16 -0
- package/dist/admin/primitives/JsonCodeEditor.d.ts +10 -0
- package/dist/admin/primitives/JsonCodeEditor.js +42 -0
- package/dist/admin/primitives/Pagination.d.ts +8 -0
- package/dist/admin/primitives/Pagination.js +8 -0
- package/dist/admin/primitives/Toolbar.d.ts +23 -0
- package/dist/admin/primitives/Toolbar.js +10 -0
- package/dist/admin/primitives/alerts.d.ts +21 -0
- package/dist/admin/primitives/alerts.js +44 -0
- package/dist/admin/sectionIds.d.ts +20 -0
- package/dist/admin/sectionIds.js +33 -0
- package/dist/admin/sections/CollectionsSection.d.ts +2 -0
- package/dist/admin/sections/CollectionsSection.js +125 -0
- package/dist/admin/sections/ContractVerifySection.d.ts +11 -0
- package/dist/admin/sections/ContractVerifySection.js +98 -0
- package/dist/admin/sections/CustomDataSection.d.ts +2 -0
- package/dist/admin/sections/CustomDataSection.js +256 -0
- package/dist/admin/sections/DataOpsSection.d.ts +26 -0
- package/dist/admin/sections/DataOpsSection.js +245 -0
- package/dist/admin/sections/HistorySection.d.ts +2 -0
- package/dist/admin/sections/HistorySection.js +26 -0
- package/dist/admin/sections/MonetizationSection.d.ts +2 -0
- package/dist/admin/sections/MonetizationSection.js +140 -0
- package/dist/admin/sections/NavigationSection.d.ts +13 -0
- package/dist/admin/sections/NavigationSection.js +195 -0
- package/dist/admin/sections/PagesSection.d.ts +2 -0
- package/dist/admin/sections/PagesSection.js +157 -0
- package/dist/admin/sections/SchemaDesignerSection.d.ts +2 -0
- package/dist/admin/sections/SchemaDesignerSection.js +167 -0
- package/dist/admin/sections/SiteSettingsSection.d.ts +12 -0
- package/dist/admin/sections/SiteSettingsSection.js +122 -0
- package/dist/admin/sections/TippingSection.d.ts +2 -0
- package/dist/admin/sections/TippingSection.js +178 -0
- package/dist/admin/sections/media/MediaDetail.d.ts +12 -0
- package/dist/admin/sections/media/MediaDetail.js +74 -0
- package/dist/admin/sections/media/MediaGrid.d.ts +14 -0
- package/dist/admin/sections/media/MediaGrid.js +22 -0
- package/dist/admin/sections/media/MediaSection.d.ts +2 -0
- package/dist/admin/sections/media/MediaSection.js +97 -0
- package/dist/admin/sections/media/MediaUploader.d.ts +7 -0
- package/dist/admin/sections/media/MediaUploader.js +72 -0
- package/dist/admin/sections/media/types.d.ts +33 -0
- package/dist/admin/sections/media/types.js +1 -0
- package/dist/admin/styles.css +533 -0
- package/dist/admin/types.d.ts +85 -0
- package/dist/admin/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/react/CmsContent.d.ts +20 -0
- package/dist/react/CmsContent.js +31 -0
- package/dist/react/entries.d.ts +9 -0
- package/dist/react/entries.js +25 -0
- package/dist/react/fetch.d.ts +11 -0
- package/dist/react/fetch.js +32 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +9 -0
- package/dist/react/metadata.d.ts +44 -0
- package/dist/react/metadata.js +142 -0
- package/dist/react/sanitize.d.ts +17 -0
- package/dist/react/sanitize.js +326 -0
- package/dist/react/server.d.ts +14 -0
- package/dist/react/server.js +13 -0
- package/dist/react/sitemap.d.ts +28 -0
- package/dist/react/sitemap.js +91 -0
- package/dist/react/slugs.d.ts +27 -0
- package/dist/react/slugs.js +52 -0
- package/dist/react/types.d.ts +85 -0
- package/dist/react/types.js +1 -0
- package/dist/react/visitor.d.ts +7 -0
- package/dist/react/visitor.js +18 -0
- package/dist/site-templates/BlogTemplates.d.ts +95 -0
- package/dist/site-templates/BlogTemplates.js +64 -0
- package/dist/site-templates/ContactPageTemplate.d.ts +14 -0
- package/dist/site-templates/ContactPageTemplate.js +5 -0
- package/dist/site-templates/DashboardOverviewTemplate.d.ts +29 -0
- package/dist/site-templates/DashboardOverviewTemplate.js +17 -0
- package/dist/site-templates/DashboardShell.d.ts +28 -0
- package/dist/site-templates/DashboardShell.js +10 -0
- package/dist/site-templates/DocsSidebar.d.ts +14 -0
- package/dist/site-templates/DocsSidebar.js +13 -0
- package/dist/site-templates/DocsTemplates.d.ts +60 -0
- package/dist/site-templates/DocsTemplates.js +47 -0
- package/dist/site-templates/HomePageTemplate.d.ts +15 -0
- package/dist/site-templates/HomePageTemplate.js +10 -0
- package/dist/site-templates/LegalPageTemplate.d.ts +12 -0
- package/dist/site-templates/LegalPageTemplate.js +6 -0
- package/dist/site-templates/MarkdownContent.d.ts +7 -0
- package/dist/site-templates/MarkdownContent.js +24 -0
- package/dist/site-templates/NotFoundTemplate.d.ts +9 -0
- package/dist/site-templates/NotFoundTemplate.js +5 -0
- package/dist/site-templates/SiteFooter.d.ts +13 -0
- package/dist/site-templates/SiteFooter.js +4 -0
- package/dist/site-templates/SiteLayout.d.ts +14 -0
- package/dist/site-templates/SiteLayout.js +6 -0
- package/dist/site-templates/TopNav.d.ts +10 -0
- package/dist/site-templates/TopNav.js +8 -0
- package/dist/site-templates/blogControls.d.ts +19 -0
- package/dist/site-templates/blogControls.js +37 -0
- package/dist/site-templates/codeBlock.d.ts +9 -0
- package/dist/site-templates/codeBlock.js +31 -0
- package/dist/site-templates/content-styles.css +410 -0
- package/dist/site-templates/contentIndex.d.ts +65 -0
- package/dist/site-templates/contentIndex.js +181 -0
- package/dist/site-templates/contentUi.d.ts +14 -0
- package/dist/site-templates/contentUi.js +24 -0
- package/dist/site-templates/docs-styles.css +259 -0
- package/dist/site-templates/docsNavigation.d.ts +18 -0
- package/dist/site-templates/docsNavigation.js +50 -0
- package/dist/site-templates/index.d.ts +28 -0
- package/dist/site-templates/index.js +25 -0
- package/dist/site-templates/monetization-styles.css +154 -0
- package/dist/site-templates/paywallControls.d.ts +22 -0
- package/dist/site-templates/paywallControls.js +9 -0
- package/dist/site-templates/routing.d.ts +12 -0
- package/dist/site-templates/routing.js +36 -0
- package/dist/site-templates/solanaAtaSetup.d.ts +11 -0
- package/dist/site-templates/solanaAtaSetup.js +38 -0
- package/dist/site-templates/solanaMicropayments.d.ts +65 -0
- package/dist/site-templates/solanaMicropayments.js +115 -0
- package/dist/site-templates/styles.css +332 -0
- package/dist/site-templates/tipControls.d.ts +24 -0
- package/dist/site-templates/tipControls.js +43 -0
- package/dist/site-templates/tocExtractor.d.ts +16 -0
- package/dist/site-templates/tocExtractor.js +58 -0
- package/dist/site-templates/tocScrollSpy.d.ts +16 -0
- package/dist/site-templates/tocScrollSpy.js +37 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +20 -0
- package/package.json +58 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PluginContext } from "../../types.js";
|
|
2
|
+
import type { Asset } from "./types.js";
|
|
3
|
+
interface MediaDetailProps {
|
|
4
|
+
asset: Asset;
|
|
5
|
+
pluginContext: PluginContext;
|
|
6
|
+
canWrite: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onDeleted: () => void;
|
|
9
|
+
onUpdated: (asset: Asset) => void;
|
|
10
|
+
}
|
|
11
|
+
export default function MediaDetail({ asset, pluginContext, canWrite, onClose, onDeleted, onUpdated }: MediaDetailProps): React.JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { requestJson } from "../../api.js";
|
|
4
|
+
import { AdminButton, StatusNotice, TextInput } from "../../components.js";
|
|
5
|
+
export default function MediaDetail({ asset, pluginContext, canWrite, onClose, onDeleted, onUpdated }) {
|
|
6
|
+
const [altText, setAltText] = useState(asset.alt_text || "");
|
|
7
|
+
const [saving, setSaving] = useState(false);
|
|
8
|
+
const [deleting, setDeleting] = useState(false);
|
|
9
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
10
|
+
const [status, setStatus] = useState(null);
|
|
11
|
+
const handleSave = useCallback(async () => {
|
|
12
|
+
setSaving(true);
|
|
13
|
+
setStatus(null);
|
|
14
|
+
try {
|
|
15
|
+
const updated = await requestJson(pluginContext, `/admin/media/assets/${asset.id}`, { method: "PUT", body: { alt_text: altText || null } });
|
|
16
|
+
onUpdated(updated);
|
|
17
|
+
setStatus({ tone: "success", message: "Saved." });
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
setStatus({ tone: "error", message: err.message });
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
setSaving(false);
|
|
24
|
+
}
|
|
25
|
+
}, [altText, asset.id, pluginContext, onUpdated]);
|
|
26
|
+
const handleDelete = useCallback(async () => {
|
|
27
|
+
setDeleting(true);
|
|
28
|
+
setStatus(null);
|
|
29
|
+
try {
|
|
30
|
+
await requestJson(pluginContext, `/admin/media/assets/${asset.id}`, {
|
|
31
|
+
method: "DELETE"
|
|
32
|
+
});
|
|
33
|
+
onDeleted();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
setStatus({ tone: "error", message: err.message });
|
|
37
|
+
setDeleting(false);
|
|
38
|
+
}
|
|
39
|
+
}, [asset.id, pluginContext, onDeleted]);
|
|
40
|
+
const originalUrl = variantUrl(asset, null);
|
|
41
|
+
return (_jsxs("div", { className: "cedros-media-detail", children: [_jsxs("div", { className: "cedros-media-detail__header", children: [_jsx("h3", { className: "cedros-data-card__title", children: asset.filename }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: onClose, children: "Close" })] }), _jsx("img", { className: "cedros-media-detail__preview", src: originalUrl, alt: asset.alt_text || asset.filename }), _jsxs("div", { className: "cedros-media-detail__meta", children: [_jsxs("div", { className: "cedros-data-key", children: [asset.content_type, " \u00B7 ", formatBytes(asset.size_bytes), asset.width && asset.height ? ` \u00B7 ${asset.width}\u00D7${asset.height}` : ""] }), _jsxs("div", { className: "cedros-data-key", style: { marginTop: "0.25rem" }, children: ["Uploaded ", new Date(asset.created_at).toLocaleDateString()] })] }), _jsxs("div", { className: "cedros-media-detail__urls", children: [_jsx("span", { className: "cedros-data-label", children: "URLs" }), _jsx(UrlRow, { label: "Original", url: originalUrl }), asset.variants.map((v) => (_jsx(UrlRow, { label: v.suffix, url: v.url }, v.suffix)))] }), _jsx(TextInput, { label: "Alt text", value: altText, onChange: (e) => setAltText(e.target.value), placeholder: "Describe this image", disabled: !canWrite }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "primary", size: "sm", onClick: () => void handleSave(), disabled: saving || !canWrite, children: saving ? "Saving..." : "Save" }), !confirmDelete ? (_jsx(AdminButton, { variant: "danger", size: "sm", onClick: () => setConfirmDelete(true), disabled: deleting || !canWrite, children: "Delete" })) : (_jsxs(_Fragment, { children: [_jsx(AdminButton, { variant: "danger", size: "sm", onClick: () => void handleDelete(), disabled: deleting, children: deleting ? "Deleting..." : "Confirm Delete" }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: () => setConfirmDelete(false), disabled: deleting, children: "Cancel" })] }))] }), status && _jsx(StatusNotice, { tone: status.tone, message: status.message })] }));
|
|
42
|
+
}
|
|
43
|
+
function UrlRow({ label, url }) {
|
|
44
|
+
const [copied, setCopied] = useState(false);
|
|
45
|
+
const copy = () => {
|
|
46
|
+
void navigator.clipboard.writeText(url).then(() => {
|
|
47
|
+
setCopied(true);
|
|
48
|
+
setTimeout(() => setCopied(false), 1500);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
return (_jsxs("div", { className: "cedros-media-url-row", children: [_jsx("span", { className: "cedros-data-pill", children: label }), _jsx("code", { className: "cedros-data-key cedros-media-url-row__url", children: url }), _jsx(AdminButton, { variant: "ghost", size: "sm", onClick: copy, children: copied ? "Copied" : "Copy" })] }));
|
|
52
|
+
}
|
|
53
|
+
function variantUrl(asset, suffix) {
|
|
54
|
+
if (suffix) {
|
|
55
|
+
const v = asset.variants.find((v) => v.suffix === suffix);
|
|
56
|
+
if (v)
|
|
57
|
+
return v.url;
|
|
58
|
+
}
|
|
59
|
+
// For original, try to find a variant with suffix matching the original format,
|
|
60
|
+
// otherwise fall back to the webp or first variant
|
|
61
|
+
const webp = asset.variants.find((v) => v.suffix === "webp");
|
|
62
|
+
if (webp)
|
|
63
|
+
return webp.url;
|
|
64
|
+
if (asset.variants.length > 0)
|
|
65
|
+
return asset.variants[0].url;
|
|
66
|
+
return asset.original_key;
|
|
67
|
+
}
|
|
68
|
+
function formatBytes(bytes) {
|
|
69
|
+
if (bytes < 1024)
|
|
70
|
+
return `${bytes} B`;
|
|
71
|
+
if (bytes < 1024 * 1024)
|
|
72
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
73
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Asset } from "./types.js";
|
|
2
|
+
interface MediaGridProps {
|
|
3
|
+
assets: Asset[];
|
|
4
|
+
selectedId: string | null;
|
|
5
|
+
onSelect: (asset: Asset) => void;
|
|
6
|
+
viewMode: "grid" | "list";
|
|
7
|
+
onViewModeChange: (mode: "grid" | "list") => void;
|
|
8
|
+
page: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
onPageChange: (page: number) => void;
|
|
11
|
+
hasMore: boolean;
|
|
12
|
+
}
|
|
13
|
+
export default function MediaGrid({ assets, selectedId, onSelect, viewMode, onViewModeChange, page, pageSize, onPageChange, hasMore }: MediaGridProps): React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AdminButton } from "../../components.js";
|
|
3
|
+
export default function MediaGrid({ assets, selectedId, onSelect, viewMode, onViewModeChange, page, pageSize, onPageChange, hasMore }) {
|
|
4
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "cedros-data-toolbar", style: { marginBottom: "0.6rem" }, children: [_jsx("div", { className: "cedros-data-toolbar__left", children: _jsxs("span", { className: "cedros-data-label", children: [assets.length, " asset", assets.length !== 1 ? "s" : ""] }) }), _jsxs("div", { className: "cedros-data-toolbar__right", children: [_jsx("button", { className: `cedros-data-chip ${viewMode === "grid" ? "cedros-data-chip--active" : ""}`, onClick: () => onViewModeChange("grid"), children: "Grid" }), _jsx("button", { className: `cedros-data-chip ${viewMode === "list" ? "cedros-data-chip--active" : ""}`, onClick: () => onViewModeChange("list"), children: "List" })] })] }), assets.length === 0 ? (_jsx("p", { className: "cedros-data-empty", children: "No assets found." })) : viewMode === "grid" ? (_jsx("div", { className: "cedros-media-grid", children: assets.map((asset) => (_jsxs("button", { className: `cedros-media-grid__item ${selectedId === asset.id ? "cedros-media-grid__item--selected" : ""}`, onClick: () => onSelect(asset), children: [_jsx("img", { className: "cedros-media-grid__thumb", src: thumbnailUrl(asset), alt: asset.alt_text || asset.filename, loading: "lazy" }), _jsx("span", { className: "cedros-media-grid__name", children: asset.filename })] }, asset.id))) })) : (_jsx("div", { className: "cedros-data-table-wrap", children: _jsxs("table", { className: "cedros-data-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Preview" }), _jsx("th", { children: "Filename" }), _jsx("th", { children: "Type" }), _jsx("th", { children: "Size" }), _jsx("th", { children: "Dimensions" })] }) }), _jsx("tbody", { children: assets.map((asset) => (_jsxs("tr", { onClick: () => onSelect(asset), style: { cursor: "pointer" }, children: [_jsx("td", { children: _jsx("img", { src: thumbnailUrl(asset), alt: asset.alt_text || asset.filename, style: { width: 40, height: 40, objectFit: "cover", borderRadius: 4 }, loading: "lazy" }) }), _jsx("td", { children: asset.filename }), _jsx("td", { children: _jsx("span", { className: "cedros-data-pill", children: asset.content_type }) }), _jsx("td", { children: formatBytes(asset.size_bytes) }), _jsx("td", { children: asset.width && asset.height ? `${asset.width}x${asset.height}` : "-" })] }, asset.id))) })] }) })), _jsxs("div", { className: "cedros-data-pagination", style: { marginTop: "0.6rem" }, children: [_jsx(AdminButton, { variant: "ghost", size: "sm", disabled: page === 0, onClick: () => onPageChange(page - 1), children: "Previous" }), _jsxs("span", { className: "cedros-data-label", children: ["Page ", page + 1] }), _jsx(AdminButton, { variant: "ghost", size: "sm", disabled: !hasMore, onClick: () => onPageChange(page + 1), children: "Next" })] })] }));
|
|
5
|
+
}
|
|
6
|
+
function thumbnailUrl(asset) {
|
|
7
|
+
const thumb = asset.variants.find((v) => v.suffix === "thumb");
|
|
8
|
+
if (thumb)
|
|
9
|
+
return thumb.url;
|
|
10
|
+
const webp = asset.variants.find((v) => v.suffix === "webp");
|
|
11
|
+
if (webp)
|
|
12
|
+
return webp.url;
|
|
13
|
+
// Fallback to constructing URL from original key
|
|
14
|
+
return asset.original_key;
|
|
15
|
+
}
|
|
16
|
+
function formatBytes(bytes) {
|
|
17
|
+
if (bytes < 1024)
|
|
18
|
+
return `${bytes} B`;
|
|
19
|
+
if (bytes < 1024 * 1024)
|
|
20
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
21
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { requestJson } from "../../api.js";
|
|
4
|
+
import { AdminButton, Card, StatusNotice } from "../../components.js";
|
|
5
|
+
import MediaDetail from "./MediaDetail.js";
|
|
6
|
+
import MediaGrid from "./MediaGrid.js";
|
|
7
|
+
import MediaUploader from "./MediaUploader.js";
|
|
8
|
+
export default function MediaSection({ pluginContext, pageSize }) {
|
|
9
|
+
const canRead = pluginContext.hasPermission("data:media:read");
|
|
10
|
+
const canWrite = pluginContext.hasPermission("data:media:write");
|
|
11
|
+
const [storageConfig, setStorageConfig] = useState(null);
|
|
12
|
+
const [configLoading, setConfigLoading] = useState(true);
|
|
13
|
+
const [configError, setConfigError] = useState("");
|
|
14
|
+
const [testResult, setTestResult] = useState(null);
|
|
15
|
+
const [testLoading, setTestLoading] = useState(false);
|
|
16
|
+
const [assets, setAssets] = useState([]);
|
|
17
|
+
const [assetsLoading, setAssetsLoading] = useState(false);
|
|
18
|
+
const [page, setPage] = useState(0);
|
|
19
|
+
const [viewMode, setViewMode] = useState("grid");
|
|
20
|
+
const [selectedAsset, setSelectedAsset] = useState(null);
|
|
21
|
+
const effectivePageSize = pageSize || 24;
|
|
22
|
+
const loadConfig = useCallback(async () => {
|
|
23
|
+
setConfigLoading(true);
|
|
24
|
+
setConfigError("");
|
|
25
|
+
try {
|
|
26
|
+
const data = await requestJson(pluginContext, "/admin/storage/config");
|
|
27
|
+
setStorageConfig(data);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
setConfigError(err.message);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
setConfigLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}, [pluginContext]);
|
|
36
|
+
const loadAssets = useCallback(async (pageNum) => {
|
|
37
|
+
setAssetsLoading(true);
|
|
38
|
+
try {
|
|
39
|
+
const data = await requestJson(pluginContext, `/admin/media/assets?limit=${effectivePageSize}&offset=${pageNum * effectivePageSize}`);
|
|
40
|
+
setAssets(data);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Silently fail on asset load - config screen will show errors
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setAssetsLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, [pluginContext, effectivePageSize]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
void loadConfig();
|
|
51
|
+
}, [loadConfig]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (storageConfig?.enabled) {
|
|
54
|
+
void loadAssets(page);
|
|
55
|
+
}
|
|
56
|
+
}, [storageConfig?.enabled, page, loadAssets]);
|
|
57
|
+
const handleTest = useCallback(async () => {
|
|
58
|
+
setTestLoading(true);
|
|
59
|
+
setTestResult(null);
|
|
60
|
+
try {
|
|
61
|
+
await requestJson(pluginContext, "/admin/storage/test", { method: "POST" });
|
|
62
|
+
setTestResult({ tone: "success", message: "Connection successful." });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
setTestResult({ tone: "error", message: err.message });
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
setTestLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, [pluginContext]);
|
|
71
|
+
const handlePageChange = useCallback((newPage) => {
|
|
72
|
+
setPage(newPage);
|
|
73
|
+
setSelectedAsset(null);
|
|
74
|
+
}, []);
|
|
75
|
+
const handleUploadComplete = useCallback(() => {
|
|
76
|
+
setPage(0);
|
|
77
|
+
void loadAssets(0);
|
|
78
|
+
}, [loadAssets]);
|
|
79
|
+
const handleAssetDeleted = useCallback(() => {
|
|
80
|
+
setSelectedAsset(null);
|
|
81
|
+
void loadAssets(page);
|
|
82
|
+
}, [page, loadAssets]);
|
|
83
|
+
const handleAssetUpdated = useCallback((updated) => {
|
|
84
|
+
setSelectedAsset(updated);
|
|
85
|
+
setAssets((prev) => prev.map((a) => (a.id === updated.id ? updated : a)));
|
|
86
|
+
}, []);
|
|
87
|
+
if (configLoading) {
|
|
88
|
+
return (_jsx("div", { className: "cedros-data", children: _jsx("p", { className: "cedros-data-empty", children: "Loading storage configuration..." }) }));
|
|
89
|
+
}
|
|
90
|
+
if (configError) {
|
|
91
|
+
return (_jsx("div", { className: "cedros-data", children: _jsx(StatusNotice, { tone: "error", message: configError }) }));
|
|
92
|
+
}
|
|
93
|
+
if (!storageConfig?.enabled) {
|
|
94
|
+
return (_jsx("div", { className: "cedros-data", children: _jsx("header", { className: "cedros-data__header", children: _jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Media Library" }), _jsxs("p", { className: "cedros-data__subtitle", children: ["Storage is not enabled. Start the server with ", _jsx("code", { children: "--enable-storage" }), " and configure ", _jsx("code", { children: "STORAGE_*" }), " environment variables."] })] }) }) }));
|
|
95
|
+
}
|
|
96
|
+
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Media Library" }), _jsxs("p", { className: "cedros-data__subtitle", children: ["Upload and manage images. Storage: ", storageConfig.config?.bucket ?? "unknown", " (", storageConfig.config?.region ?? "?", ")"] }), !canRead && (_jsxs("p", { className: "cedros-data__subtitle", children: ["Read-only mode. Missing ", _jsx("code", { children: "data:media:read" }), " permission."] }))] }), _jsx("div", { className: "cedros-data-actions", children: _jsx(AdminButton, { variant: "secondary", size: "sm", onClick: () => void handleTest(), disabled: testLoading || !canWrite, children: testLoading ? "Testing..." : "Test Connection" }) })] }), testResult && _jsx(StatusNotice, { tone: testResult.tone, message: testResult.message }), canWrite && (_jsx(MediaUploader, { pluginContext: pluginContext, onUploadComplete: handleUploadComplete })), _jsx(Card, { title: "Assets", subtitle: assetsLoading ? "Loading..." : undefined, children: selectedAsset ? (_jsx(MediaDetail, { asset: selectedAsset, pluginContext: pluginContext, canWrite: canWrite, onClose: () => setSelectedAsset(null), onDeleted: handleAssetDeleted, onUpdated: handleAssetUpdated })) : (_jsx(MediaGrid, { assets: assets, selectedId: selectedAsset ? selectedAsset.id : null, onSelect: setSelectedAsset, viewMode: viewMode, onViewModeChange: setViewMode, page: page, pageSize: effectivePageSize, onPageChange: handlePageChange, hasMore: assets.length === effectivePageSize })) })] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginContext } from "../../types.js";
|
|
2
|
+
interface MediaUploaderProps {
|
|
3
|
+
pluginContext: PluginContext;
|
|
4
|
+
onUploadComplete: () => void;
|
|
5
|
+
}
|
|
6
|
+
export default function MediaUploader({ pluginContext, onUploadComplete }: MediaUploaderProps): React.JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
import { AdminButton, StatusNotice } from "../../components.js";
|
|
4
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
5
|
+
const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
6
|
+
export default function MediaUploader({ pluginContext, onUploadComplete }) {
|
|
7
|
+
const [dragOver, setDragOver] = useState(false);
|
|
8
|
+
const [uploading, setUploading] = useState(false);
|
|
9
|
+
const [progress, setProgress] = useState("");
|
|
10
|
+
const [error, setError] = useState("");
|
|
11
|
+
const inputRef = useRef(null);
|
|
12
|
+
const uploadFile = useCallback(async (file) => {
|
|
13
|
+
setError("");
|
|
14
|
+
if (!ACCEPTED_TYPES.includes(file.type)) {
|
|
15
|
+
setError(`Unsupported file type: ${file.type}. Accepted: JPEG, PNG, GIF, WebP.`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
19
|
+
setError(`File exceeds 10 MB limit (${(file.size / (1024 * 1024)).toFixed(1)} MB).`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
setUploading(true);
|
|
23
|
+
setProgress(`Uploading ${file.name}...`);
|
|
24
|
+
try {
|
|
25
|
+
const formData = new FormData();
|
|
26
|
+
formData.append("file", file);
|
|
27
|
+
const token = pluginContext.getAccessToken();
|
|
28
|
+
const headers = {};
|
|
29
|
+
if (token)
|
|
30
|
+
headers.Authorization = `Bearer ${token}`;
|
|
31
|
+
if (pluginContext.orgId)
|
|
32
|
+
headers["x-cedros-org-id"] = pluginContext.orgId;
|
|
33
|
+
const baseUrl = pluginContext.serverUrl.replace(/\/+$/, "");
|
|
34
|
+
const response = await fetch(`${baseUrl}/admin/media/upload`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers,
|
|
37
|
+
body: formData
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
throw new Error(text || `Upload failed (${response.status})`);
|
|
42
|
+
}
|
|
43
|
+
setProgress("");
|
|
44
|
+
onUploadComplete();
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
setError(err.message);
|
|
48
|
+
setProgress("");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setUploading(false);
|
|
52
|
+
}
|
|
53
|
+
}, [pluginContext, onUploadComplete]);
|
|
54
|
+
const handleDrop = useCallback((e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
setDragOver(false);
|
|
57
|
+
const file = e.dataTransfer.files[0];
|
|
58
|
+
if (file)
|
|
59
|
+
void uploadFile(file);
|
|
60
|
+
}, [uploadFile]);
|
|
61
|
+
const handleFileInput = useCallback((e) => {
|
|
62
|
+
const file = e.target.files?.[0];
|
|
63
|
+
if (file)
|
|
64
|
+
void uploadFile(file);
|
|
65
|
+
if (inputRef.current)
|
|
66
|
+
inputRef.current.value = "";
|
|
67
|
+
}, [uploadFile]);
|
|
68
|
+
return (_jsxs("div", { className: "cedros-data-card", children: [_jsxs("div", { className: `cedros-media-dropzone ${dragOver ? "cedros-media-dropzone--active" : ""}`, onDragOver: (e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
setDragOver(true);
|
|
71
|
+
}, onDragLeave: () => setDragOver(false), onDrop: handleDrop, children: [_jsx("p", { className: "cedros-media-dropzone__text", children: uploading ? progress : "Drag & drop an image here, or click to browse" }), _jsx(AdminButton, { variant: "secondary", size: "sm", disabled: uploading, onClick: () => inputRef.current?.click(), children: "Choose File" }), _jsx("input", { ref: inputRef, type: "file", accept: ACCEPTED_TYPES.join(","), style: { display: "none" }, onChange: handleFileInput }), _jsx("p", { className: "cedros-media-dropzone__hint", children: "JPEG, PNG, GIF, WebP \u00B7 Max 10 MB" })] }), error && _jsx("div", { className: "cedros-data-card__body", children: _jsx(StatusNotice, { tone: "error", message: error }) })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface AssetVariant {
|
|
2
|
+
suffix: string;
|
|
3
|
+
key: string;
|
|
4
|
+
url: string;
|
|
5
|
+
content_type: string;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Asset {
|
|
10
|
+
id: string;
|
|
11
|
+
filename: string;
|
|
12
|
+
original_key: string;
|
|
13
|
+
content_type: string;
|
|
14
|
+
size_bytes: number;
|
|
15
|
+
width: number | null;
|
|
16
|
+
height: number | null;
|
|
17
|
+
variants: AssetVariant[];
|
|
18
|
+
alt_text: string | null;
|
|
19
|
+
metadata: Record<string, unknown>;
|
|
20
|
+
created_at: string;
|
|
21
|
+
updated_at: string;
|
|
22
|
+
}
|
|
23
|
+
export interface StorageConfigResponse {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
config?: {
|
|
26
|
+
bucket: string;
|
|
27
|
+
region: string;
|
|
28
|
+
endpoint: string | null;
|
|
29
|
+
cdn_base_url: string | null;
|
|
30
|
+
path_prefix: string | null;
|
|
31
|
+
access_key: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|