@cedros/data-react 0.1.0 → 0.1.2
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 +58 -0
- package/README.md +96 -3
- package/dist/admin/index.d.ts +5 -5
- package/dist/admin/primitives/ConfirmDialog.d.ts +1 -2
- package/dist/admin/primitives/JsonCodeEditor.d.ts +1 -2
- package/dist/admin/primitives/Pagination.d.ts +2 -3
- package/dist/admin/primitives/Toolbar.d.ts +3 -4
- package/dist/admin/sections/TippingSection.js +1 -26
- package/dist/react/index.d.ts +1 -1
- package/dist/react/metadata.d.ts +6 -4
- package/dist/react/metadata.js +34 -24
- package/dist/react/server.d.ts +1 -1
- package/dist/react/types.d.ts +12 -0
- package/dist/site-templates/BlogTemplates.d.ts +8 -4
- package/dist/site-templates/BlogTemplates.js +2 -2
- package/dist/site-templates/content-styles.css +8 -0
- package/dist/site-templates/docs-styles.css +6 -0
- package/dist/site-templates/index.d.ts +2 -4
- package/dist/site-templates/index.js +1 -3
- package/dist/site-templates/tipControls.d.ts +30 -13
- package/dist/site-templates/tipControls.js +133 -19
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### Added
|
|
6
|
+
- `TipWidget` self-configuration: fetches tipping config from `GET {dataServerUrl}/site/config/tipping` when no `recipient` prop is provided.
|
|
7
|
+
- `TipWidget` wallet auto-detection: connects to browser Solana wallets (Phantom, Solflare) automatically.
|
|
8
|
+
- `SOL_CURRENCY` and `USDC_CURRENCY` constants now include `mint` addresses for use with `@cedros/trade-react`.
|
|
9
|
+
- `@cedros/trade-react` added as optional peer dependency for tipping payment flow.
|
|
10
|
+
- `BlogSearchInput` — CMD+K keyboard shortcut with debounced client-side search.
|
|
11
|
+
- `FilterDimensionChips` — generic dimension-based tag filtering for blog index.
|
|
12
|
+
- `BookmarkButton` — per-post bookmark toggle.
|
|
13
|
+
- `ContentPaywall` — metered/locked/preview paywall for blog posts.
|
|
14
|
+
- `BlogIndexTemplate` props: `filterDimensions`, `activeFilters`, `onFilterChange`, `onSearch`, `bookmarkedSlugs`, `onBookmarkToggle`.
|
|
15
|
+
- `BlogPostTemplate` props: `isBookmarked`, `onBookmarkToggle`, `slug`, `paywall`.
|
|
16
|
+
- `BlogPaywallConfig` type for paywall configuration including metered free reads.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `TipWidget` now delegates payment to `TradeApiClient` from `@cedros/trade-react` (build → sign → execute) instead of building Solana transactions directly.
|
|
20
|
+
- `TipWidgetProps.recipient` is now optional (enables self-configuration mode).
|
|
21
|
+
- `BlogTippingConfig` replaced `payClient: TipPayClient` with `dataServerUrl`, `senderAddress`, and `signTransaction` props.
|
|
22
|
+
- `SOL_CURRENCY.mint` changed from `undefined` to `"So11111111111111111111111111111111111111112"`.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
- `SolanaMicropayments` component and `createSolanaTipClient`/`detectSolanaWallet` helpers — replaced by `TipWidget` + `@cedros/trade-react`.
|
|
26
|
+
- `ensureRecipientAta` helper — ATA creation is now handled by the trade-react build endpoint (`createsAta: true`).
|
|
27
|
+
- `solanaAtaSetup.ts` and `solanaMicropayments.tsx` files.
|
|
28
|
+
- `TipPayClient` and `TipParams` types — no longer needed.
|
|
29
|
+
- `@solana/web3.js` and `@solana/spl-token` removed from package dependencies.
|
|
30
|
+
- "Ensure Recipient ATA" button removed from admin TippingSection.
|
|
31
|
+
|
|
32
|
+
### Migration Notes
|
|
33
|
+
|
|
34
|
+
**Tipping (breaking):**
|
|
35
|
+
- Before:
|
|
36
|
+
```tsx
|
|
37
|
+
import { SolanaMicropayments } from "@cedros/data-react/site-templates";
|
|
38
|
+
<SolanaMicropayments recipient="..." rpcEndpoint="..." />
|
|
39
|
+
```
|
|
40
|
+
Or with BlogPostTemplate:
|
|
41
|
+
```tsx
|
|
42
|
+
tipping={{ enabled: true, recipient: "...", currencies: [...], payClient: createSolanaTipClient() }}
|
|
43
|
+
```
|
|
44
|
+
- After:
|
|
45
|
+
```tsx
|
|
46
|
+
import { TipWidget } from "@cedros/data-react/site-templates";
|
|
47
|
+
<TipWidget recipient="..." />
|
|
48
|
+
// or self-configuring:
|
|
49
|
+
<TipWidget dataServerUrl="https://..." />
|
|
50
|
+
```
|
|
51
|
+
With BlogPostTemplate:
|
|
52
|
+
```tsx
|
|
53
|
+
tipping={{ enabled: true, dataServerUrl: "https://..." }}
|
|
54
|
+
// or manual:
|
|
55
|
+
tipping={{ enabled: true, recipient: "...", currencies: [...] }}
|
|
56
|
+
```
|
|
57
|
+
Requires `npm install @cedros/trade-react` for tipping to work at runtime.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### Previously shipped (included for completeness)
|
|
62
|
+
|
|
5
63
|
### Added
|
|
6
64
|
- `MarkdownContent` renderer based on `react-markdown` + `remark-gfm` + `rehype-slug`.
|
|
7
65
|
- `bodyMarkdown` support in:
|
package/README.md
CHANGED
|
@@ -40,7 +40,10 @@ Packaging smoke:
|
|
|
40
40
|
- admin components/primitives
|
|
41
41
|
- `@cedros/data-react/site-templates`
|
|
42
42
|
- site shell/layout components
|
|
43
|
-
- page templates
|
|
43
|
+
- page templates (blog, docs, home, contact, legal, not-found, dashboard)
|
|
44
|
+
- blog features: `BlogSearchInput`, `FilterDimensionChips`, `BookmarkButton`
|
|
45
|
+
- `TipWidget`, `SOL_CURRENCY`, `USDC_CURRENCY`
|
|
46
|
+
- `ContentPaywall`
|
|
44
47
|
- routing/content helpers
|
|
45
48
|
- `@cedros/data-react/admin/styles.css`
|
|
46
49
|
- `@cedros/data-react/site-templates/styles.css`
|
|
@@ -127,8 +130,18 @@ Core page templates:
|
|
|
127
130
|
- `DashboardOverviewTemplate`
|
|
128
131
|
|
|
129
132
|
Blog templates:
|
|
130
|
-
- `BlogIndexTemplate`
|
|
131
|
-
- `BlogPostTemplate`
|
|
133
|
+
- `BlogIndexTemplate` — supports search, category/tag filters, bookmarks, and generic filter dimensions
|
|
134
|
+
- `BlogPostTemplate` — supports tipping, paywall, and bookmarks
|
|
135
|
+
|
|
136
|
+
Blog interactive features:
|
|
137
|
+
- `BlogSearchInput` — CMD+K keyboard shortcut, debounced client-side search
|
|
138
|
+
- `FilterDimensionChips` — generic dimension-based tag filtering (replaces category/tag dropdowns)
|
|
139
|
+
- `BookmarkButton` — per-post bookmark toggle (state managed by consumer)
|
|
140
|
+
- `ContentPaywall` — metered/locked/preview paywall with purchase flow via `PaywallPayClient`
|
|
141
|
+
|
|
142
|
+
Tipping:
|
|
143
|
+
- `TipWidget` — self-configuring tip widget with currency selection, preset amounts, and send flow
|
|
144
|
+
- `SOL_CURRENCY`, `USDC_CURRENCY` — predefined currency constants
|
|
132
145
|
|
|
133
146
|
Docs templates:
|
|
134
147
|
- `DocsIndexTemplate`
|
|
@@ -144,8 +157,88 @@ Content rendering and helpers:
|
|
|
144
157
|
- `prepareBlogIndex`
|
|
145
158
|
- `prepareDocsIndex`
|
|
146
159
|
- `collectFilterValues`
|
|
160
|
+
- `collectDimensionValues`
|
|
161
|
+
- `matchesFilterDimensions`
|
|
147
162
|
- `buildContentListHref`
|
|
148
163
|
|
|
164
|
+
## Tipping (TipWidget)
|
|
165
|
+
|
|
166
|
+
`TipWidget` delegates payment to `@cedros/trade-react` and supports two modes:
|
|
167
|
+
|
|
168
|
+
**Self-configuring** — fetches config from your cedros-data server:
|
|
169
|
+
```tsx
|
|
170
|
+
<TipWidget dataServerUrl="https://your-data-server.com" />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Manual** — provide recipient directly (skips config fetch):
|
|
174
|
+
```tsx
|
|
175
|
+
<TipWidget recipient="SomeWalletAddress" currencies={[SOL_CURRENCY]} />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**With per-post recipient override:**
|
|
179
|
+
```tsx
|
|
180
|
+
<TipWidget recipient={post.tipRecipient} />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The widget auto-detects browser wallets (Phantom, Solflare) for signing. To use a custom signer:
|
|
184
|
+
```tsx
|
|
185
|
+
<TipWidget
|
|
186
|
+
recipient="..."
|
|
187
|
+
senderAddress={walletAddress}
|
|
188
|
+
signTransaction={async (base64Tx) => signedBase64Tx}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Optional peer dependency
|
|
193
|
+
|
|
194
|
+
Tipping requires `@cedros/trade-react` at runtime:
|
|
195
|
+
```bash
|
|
196
|
+
npm install @cedros/trade-react
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Browser wallet auto-signing also requires `@solana/web3.js` for transaction deserialization. If not installed, provide a `signTransaction` prop instead.
|
|
200
|
+
|
|
201
|
+
### BlogPostTemplate tipping integration
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<BlogPostTemplate
|
|
205
|
+
tipping={{
|
|
206
|
+
enabled: true,
|
|
207
|
+
dataServerUrl: "https://your-data-server.com",
|
|
208
|
+
// OR provide recipient directly:
|
|
209
|
+
// recipient: "WalletAddress",
|
|
210
|
+
// currencies: [SOL_CURRENCY, USDC_CURRENCY],
|
|
211
|
+
// presets: { SOL: [0.01, 0.05, 0.1], USDC: [1, 5, 10] },
|
|
212
|
+
}}
|
|
213
|
+
{...otherProps}
|
|
214
|
+
/>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Paywall (ContentPaywall)
|
|
218
|
+
|
|
219
|
+
`BlogPostTemplate` supports metered/locked/preview paywalls:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<BlogPostTemplate
|
|
223
|
+
paywall={{
|
|
224
|
+
mode: "preview", // "free" | "preview" | "locked"
|
|
225
|
+
previewParagraphs: 3,
|
|
226
|
+
price: { amount: 5, currency: "USDC", label: "$5" },
|
|
227
|
+
unlocked: false,
|
|
228
|
+
payClient: myPaywallClient,
|
|
229
|
+
remainingFreeReads: 2, // optional metering
|
|
230
|
+
}}
|
|
231
|
+
{...otherProps}
|
|
232
|
+
/>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Modes:
|
|
236
|
+
- `free` — full content, no paywall
|
|
237
|
+
- `preview` — shows first N paragraphs + purchase prompt
|
|
238
|
+
- `locked` — no content shown, purchase prompt only
|
|
239
|
+
|
|
240
|
+
When `remainingFreeReads > 0`, metered users see full content with a remaining-reads banner.
|
|
241
|
+
|
|
149
242
|
## Markdown and HTML behavior
|
|
150
243
|
|
|
151
244
|
Docs/blog templates default to `bodyMarkdown`.
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export type { AdminPlugin, AdminSectionConfig, AdminGroupConfig, AdminSectionProps, HostContext, PluginContext, PluginRegistry, PluginId, SectionId, QualifiedSectionId, PluginPermission } from "./types.js";
|
|
1
|
+
export type { AdminPlugin, AdminSectionConfig, AdminGroupConfig, AdminSectionProps, HostContext, PluginContext, PluginRegistry, PluginId, SectionId, QualifiedSectionId, PluginPermission, ResolvedSection } from "./types.js";
|
|
2
2
|
export { cedrosDataPlugin, dataPlugin } from "./plugin.js";
|
|
3
3
|
export { default as defaultDataPlugin } from "./plugin.js";
|
|
4
4
|
export { CEDROS_DATA_SECTIONS, CEDROS_DATA_SECTION_IDS, CEDROS_DATA_GROUPS } from "./sectionIds.js";
|
|
5
5
|
export { AdminButton, Card, TextInput, SelectInput, JsonEditor, StatusNotice } from "./components.js";
|
|
6
6
|
export { DataTable, type DataTableProps, type DataTableColumn } from "./primitives/DataTable.js";
|
|
7
|
-
export { Toolbar, SearchInput, FilterChips, type FilterChipItem } from "./primitives/Toolbar.js";
|
|
8
|
-
export { Pagination } from "./primitives/Pagination.js";
|
|
9
|
-
export { JsonCodeEditor } from "./primitives/JsonCodeEditor.js";
|
|
7
|
+
export { Toolbar, SearchInput, FilterChips, type ToolbarProps, type SearchInputProps, type FilterChipsProps, type FilterChipItem } from "./primitives/Toolbar.js";
|
|
8
|
+
export { Pagination, type AdminPaginationProps } from "./primitives/Pagination.js";
|
|
9
|
+
export { JsonCodeEditor, type JsonCodeEditorProps } from "./primitives/JsonCodeEditor.js";
|
|
10
10
|
export { DiffViewer, type DiffBlock } from "./primitives/DiffViewer.js";
|
|
11
11
|
export { FormFieldRow, FieldTypeSelect, type FormFieldRowValue, type FieldTypeOption } from "./primitives/FormFieldRow.js";
|
|
12
|
-
export { ConfirmDialog } from "./primitives/ConfirmDialog.js";
|
|
12
|
+
export { ConfirmDialog, type ConfirmDialogProps } from "./primitives/ConfirmDialog.js";
|
|
13
13
|
export { ToastProvider, useToast, InlineAlert, type AlertTone } from "./primitives/alerts.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
interface ConfirmDialogProps {
|
|
2
|
+
export interface ConfirmDialogProps {
|
|
3
3
|
open: boolean;
|
|
4
4
|
title: string;
|
|
5
5
|
description?: string;
|
|
@@ -11,4 +11,3 @@ interface ConfirmDialogProps {
|
|
|
11
11
|
children?: ReactNode;
|
|
12
12
|
}
|
|
13
13
|
export declare function ConfirmDialog({ open, title, description, confirmLabel, cancelLabel, tone, onConfirm, onCancel, children }: ConfirmDialogProps): React.JSX.Element | null;
|
|
14
|
-
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
interface JsonCodeEditorProps {
|
|
1
|
+
export interface JsonCodeEditorProps {
|
|
2
2
|
value: string;
|
|
3
3
|
onChange: (value: string) => void;
|
|
4
4
|
label?: string;
|
|
@@ -7,4 +7,3 @@ interface JsonCodeEditorProps {
|
|
|
7
7
|
onValidJson?: (value: unknown) => void;
|
|
8
8
|
}
|
|
9
9
|
export declare function JsonCodeEditor({ value, onChange, label, placeholder, readOnly, onValidJson }: JsonCodeEditorProps): React.JSX.Element;
|
|
10
|
-
export {};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
interface
|
|
1
|
+
export interface AdminPaginationProps {
|
|
2
2
|
page: number;
|
|
3
3
|
pageSize: number;
|
|
4
4
|
total: number;
|
|
5
5
|
onPageChange: (page: number) => void;
|
|
6
6
|
}
|
|
7
|
-
export declare function Pagination({ page, pageSize, total, onPageChange }:
|
|
8
|
-
export {};
|
|
7
|
+
export declare function Pagination({ page, pageSize, total, onPageChange }: AdminPaginationProps): React.JSX.Element;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
interface ToolbarProps {
|
|
2
|
+
export interface ToolbarProps {
|
|
3
3
|
left?: ReactNode;
|
|
4
4
|
right?: ReactNode;
|
|
5
5
|
}
|
|
6
6
|
export declare function Toolbar({ left, right }: ToolbarProps): React.JSX.Element;
|
|
7
|
-
interface SearchInputProps {
|
|
7
|
+
export interface SearchInputProps {
|
|
8
8
|
value: string;
|
|
9
9
|
onChange: (value: string) => void;
|
|
10
10
|
placeholder?: string;
|
|
@@ -15,9 +15,8 @@ export interface FilterChipItem {
|
|
|
15
15
|
label: string;
|
|
16
16
|
active: boolean;
|
|
17
17
|
}
|
|
18
|
-
interface FilterChipsProps {
|
|
18
|
+
export interface FilterChipsProps {
|
|
19
19
|
items: FilterChipItem[];
|
|
20
20
|
onToggle: (key: string) => void;
|
|
21
21
|
}
|
|
22
22
|
export declare function FilterChips({ items, onToggle }: FilterChipsProps): React.JSX.Element;
|
|
23
|
-
export {};
|
|
@@ -101,37 +101,12 @@ export default function TippingSection({ pluginContext }) {
|
|
|
101
101
|
}
|
|
102
102
|
await save(normalizePayload(parsed));
|
|
103
103
|
}, [rawEditor, save]);
|
|
104
|
-
const handleEnsureAta = useCallback(async () => {
|
|
105
|
-
const splCurrencies = config.currencies.filter((c) => c.mint && c.enabled);
|
|
106
|
-
if (!config.recipient || splCurrencies.length === 0)
|
|
107
|
-
return;
|
|
108
|
-
setLoading(true);
|
|
109
|
-
setStatus("");
|
|
110
|
-
setTone("neutral");
|
|
111
|
-
try {
|
|
112
|
-
const { ensureRecipientAta } = await import("../../site-templates/solanaAtaSetup.js");
|
|
113
|
-
const results = [];
|
|
114
|
-
for (const cur of splCurrencies) {
|
|
115
|
-
const ata = await ensureRecipientAta(config.recipient, cur.mint);
|
|
116
|
-
results.push(`${cur.symbol}: ${ata}`);
|
|
117
|
-
}
|
|
118
|
-
setStatus(`ATAs ready: ${results.join(", ")}`);
|
|
119
|
-
setTone("success");
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
setStatus(`ATA creation failed: ${error.message}`);
|
|
123
|
-
setTone("error");
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
setLoading(false);
|
|
127
|
-
}
|
|
128
|
-
}, [config.recipient, config.currencies]);
|
|
129
104
|
return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Tipping" }), _jsx("p", { className: "cedros-data__subtitle", children: "Configure tipping widget for your site." }), !canWrite && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only mode. Missing `data:settings:write` permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void saveForm(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save Form" }), _jsx(AdminButton, { variant: "secondary", onClick: () => void applyRaw(), disabled: loading || !canWrite, title: !canWrite ? "Requires data:settings:write" : undefined, children: "Save JSON" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Tipping Settings", subtitle: "Basic tipping configuration.", children: [_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: config.enabled, onChange: (e) => setConfig((prev) => ({ ...prev, enabled: e.target.checked })), disabled: !canWrite }), "Enabled"] }), _jsx(TextInput, { label: "Recipient", value: config.recipient, onChange: (e) => setConfig((prev) => ({ ...prev, recipient: e.target.value })), placeholder: "Wallet address or identifier", disabled: !canWrite }), _jsx(TextInput, { label: "Label", value: config.label, onChange: (e) => setConfig((prev) => ({ ...prev, label: e.target.value })), placeholder: "Leave a tip", disabled: !canWrite }), _jsx(TextInput, { label: "Description", value: config.description, onChange: (e) => setConfig((prev) => ({ ...prev, description: e.target.value })), placeholder: "Optional description", disabled: !canWrite }), _jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: config.allowPerPostRecipient, onChange: (e) => setConfig((prev) => ({ ...prev, allowPerPostRecipient: e.target.checked })), disabled: !canWrite }), "Allow per-post recipient"] }), _jsx("p", { className: "cedros-data__subtitle", style: { margin: 0 }, children: "When enabled, individual blog posts can override the site-wide tip recipient." }), _jsx("h4", { style: { margin: "1rem 0 0.5rem" }, children: "Currencies" }), config.currencies.map((cur) => (_jsxs("label", { className: "cedros-data-checkbox-row", children: [_jsx("input", { type: "checkbox", checked: cur.enabled, disabled: !canWrite || cur.symbol === "SOL", onChange: (e) => {
|
|
130
105
|
setConfig((prev) => ({
|
|
131
106
|
...prev,
|
|
132
107
|
currencies: prev.currencies.map((c) => c.symbol === cur.symbol ? { ...c, enabled: e.target.checked } : c),
|
|
133
108
|
}));
|
|
134
|
-
} }), cur.symbol, cur.symbol === "SOL" ? " (always enabled)" : ""] }, cur.symbol)))
|
|
109
|
+
} }), cur.symbol, cur.symbol === "SOL" ? " (always enabled)" : ""] }, cur.symbol)))] }), _jsx(Card, { title: "Tipping JSON", subtitle: "Advanced editor for currencies, presets, and more.", children: _jsx(JsonEditor, { label: "site_settings/tipping", value: rawEditor, onChange: (e) => setRawEditor(e.target.value), disabled: !canWrite }) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
|
|
135
110
|
}
|
|
136
111
|
function normalizePayload(value) {
|
|
137
112
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
package/dist/react/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
export { CmsContent, type CmsContentProps } from "./CmsContent.js";
|
|
8
8
|
export { getOrCreateVisitorId } from "./visitor.js";
|
|
9
9
|
export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
|
|
10
|
-
export type { CmsPageRecord, SiteDataRecord, SanitizeOptions, ContentType, } from "./types.js";
|
|
10
|
+
export type { CmsPageRecord, PageMetadataInput, SiteDataRecord, SanitizeOptions, ContentType, MeteredReadsInfo, } from "./types.js";
|
package/dist/react/metadata.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildPageMetadataOptions, CmsPageRecord, ServerFetchOptions } from "./types.js";
|
|
1
|
+
import type { BuildPageMetadataOptions, CmsPageRecord, PageMetadataInput, ServerFetchOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Metadata object compatible with Next.js `generateMetadata` return type.
|
|
4
4
|
* Kept as a plain object so this module doesn't import `next` as a dependency.
|
|
@@ -26,13 +26,15 @@ export interface PageMetadata {
|
|
|
26
26
|
other?: Record<string, string>;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Builds a Next.js-compatible Metadata object from a
|
|
29
|
+
* Builds a Next.js-compatible Metadata object from a page record.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
31
|
+
* Accepts both wrapped CmsPageRecord shapes and flat page records.
|
|
32
|
+
*
|
|
33
|
+
* @param page - Page record (wrapped or flat)
|
|
32
34
|
* @param options - Optional site data, origin, path, and mode
|
|
33
35
|
* @returns Metadata object suitable for Next.js `generateMetadata`
|
|
34
36
|
*/
|
|
35
|
-
export declare function buildPageMetadata(page: CmsPageRecord, options?: BuildPageMetadataOptions): PageMetadata;
|
|
37
|
+
export declare function buildPageMetadata(page: CmsPageRecord | PageMetadataInput, options?: BuildPageMetadataOptions): PageMetadata;
|
|
36
38
|
/**
|
|
37
39
|
* Convenience wrapper that fetches a page by slug, then builds metadata.
|
|
38
40
|
*
|
package/dist/react/metadata.js
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
import { fetchJson, resolveServerUrl } from "./fetch.js";
|
|
2
|
+
/** Resolves a field from the wrapped payload or flat top-level. */
|
|
3
|
+
function field(page, key) {
|
|
4
|
+
const rec = page;
|
|
5
|
+
const payload = rec.payload;
|
|
6
|
+
return payload?.[key] ?? rec[key];
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
|
-
* Builds a Next.js-compatible Metadata object from a
|
|
9
|
+
* Builds a Next.js-compatible Metadata object from a page record.
|
|
10
|
+
*
|
|
11
|
+
* Accepts both wrapped CmsPageRecord shapes and flat page records.
|
|
4
12
|
*
|
|
5
|
-
* @param page -
|
|
13
|
+
* @param page - Page record (wrapped or flat)
|
|
6
14
|
* @param options - Optional site data, origin, path, and mode
|
|
7
15
|
* @returns Metadata object suitable for Next.js `generateMetadata`
|
|
8
16
|
*/
|
|
9
17
|
export function buildPageMetadata(page, options) {
|
|
10
|
-
const p = page.payload;
|
|
11
18
|
const site = options?.siteData;
|
|
12
19
|
const origin = options?.origin?.replace(/\/+$/, "") ?? "";
|
|
13
|
-
const path = options?.path ??
|
|
20
|
+
const path = options?.path ?? field(page, "route") ?? "";
|
|
14
21
|
const mode = options?.mode ?? "page";
|
|
15
22
|
const siteName = site?.siteName ?? site?.siteTitle ?? "";
|
|
16
|
-
const title =
|
|
17
|
-
const description =
|
|
18
|
-
const ogTitle =
|
|
19
|
-
const ogDescription =
|
|
20
|
-
const ogImages = buildOgImages(
|
|
23
|
+
const title = field(page, "title") ?? siteName ?? undefined;
|
|
24
|
+
const description = field(page, "description") ?? site?.defaultDescription ?? undefined;
|
|
25
|
+
const ogTitle = field(page, "ogTitle") ?? field(page, "title") ?? undefined;
|
|
26
|
+
const ogDescription = field(page, "ogDescription") ?? field(page, "description") ?? undefined;
|
|
27
|
+
const ogImages = buildOgImages(field(page, "ogImage"), site?.defaultOgImage);
|
|
21
28
|
const pageUrl = origin ? `${origin}${path}` : undefined;
|
|
22
29
|
const ogType = mode === "blog" ? "article" : "website";
|
|
23
|
-
const twitterTitle =
|
|
24
|
-
const twitterDescription =
|
|
25
|
-
const canonical = resolveCanonical(origin, path, mode,
|
|
30
|
+
const twitterTitle = field(page, "twitterTitle") ?? field(page, "ogTitle") ?? field(page, "title") ?? undefined;
|
|
31
|
+
const twitterDescription = field(page, "twitterDescription") ?? field(page, "ogDescription") ?? field(page, "description") ?? undefined;
|
|
32
|
+
const canonical = resolveCanonical(origin, path, mode, field(page, "slug"));
|
|
26
33
|
const metadata = {
|
|
27
34
|
title,
|
|
28
35
|
description,
|
|
@@ -112,31 +119,34 @@ function resolveCanonical(origin, path, mode, slug) {
|
|
|
112
119
|
if (!origin)
|
|
113
120
|
return undefined;
|
|
114
121
|
if (mode === "docs" && slug) {
|
|
115
|
-
return `${origin}/
|
|
122
|
+
return `${origin}/docs/${slug}`;
|
|
116
123
|
}
|
|
117
124
|
return `${origin}${path}`;
|
|
118
125
|
}
|
|
119
126
|
function buildBlogJsonLd(page, origin, path) {
|
|
120
|
-
const
|
|
127
|
+
const updatedAt = field(page, "updatedAt");
|
|
121
128
|
const jsonLd = {
|
|
122
129
|
"@context": "https://schema.org",
|
|
123
130
|
"@type": "BlogPosting",
|
|
124
|
-
headline:
|
|
125
|
-
description:
|
|
126
|
-
datePublished:
|
|
127
|
-
dateModified:
|
|
131
|
+
headline: field(page, "title"),
|
|
132
|
+
description: field(page, "description"),
|
|
133
|
+
datePublished: field(page, "publishedAt"),
|
|
134
|
+
dateModified: updatedAt ?? page.updated_at,
|
|
128
135
|
};
|
|
129
136
|
if (origin) {
|
|
130
137
|
jsonLd.url = `${origin}${path}`;
|
|
131
138
|
}
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
const author = field(page, "author");
|
|
140
|
+
if (author) {
|
|
141
|
+
jsonLd.author = { "@type": "Person", name: author };
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
const ogImage = field(page, "ogImage");
|
|
144
|
+
if (ogImage) {
|
|
145
|
+
jsonLd.image = ogImage;
|
|
137
146
|
}
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
const wordCount = field(page, "wordCount");
|
|
148
|
+
if (wordCount) {
|
|
149
|
+
jsonLd.wordCount = wordCount;
|
|
140
150
|
}
|
|
141
151
|
return JSON.stringify(jsonLd);
|
|
142
152
|
}
|
package/dist/react/server.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export { buildPageMetadata, generatePageMetadata, type PageMetadata, } from "./m
|
|
|
11
11
|
export { loadSitemapEntries } from "./sitemap.js";
|
|
12
12
|
export { fetchBlogPost } from "./entries.js";
|
|
13
13
|
export { listBlogSlugs, listContentSlugs, listLearnPathIds, } from "./slugs.js";
|
|
14
|
-
export type { BuildPageMetadataOptions, CmsPageRecord, MeteredReadsInfo, SiteDataRecord, SitemapEntry, ServerFetchOptions, ContentType, } from "./types.js";
|
|
14
|
+
export type { BuildPageMetadataOptions, CmsPageRecord, MeteredReadsInfo, PageMetadataInput, SiteDataRecord, SitemapEntry, ServerFetchOptions, ContentType, } from "./types.js";
|
package/dist/react/types.d.ts
CHANGED
|
@@ -63,6 +63,18 @@ export interface SanitizeOptions {
|
|
|
63
63
|
/** Allowed URL protocols for href/src attributes. */
|
|
64
64
|
allowedProtocols?: string[];
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Flexible input for `buildPageMetadata`.
|
|
68
|
+
*
|
|
69
|
+
* Accepts both the wrapped CmsPageRecord shape (`{ entry_key, payload, updated_at }`)
|
|
70
|
+
* and flat page records where fields live at the top level (`{ title, ogImage, ... }`).
|
|
71
|
+
*/
|
|
72
|
+
export interface PageMetadataInput {
|
|
73
|
+
entry_key?: string;
|
|
74
|
+
payload?: Record<string, unknown>;
|
|
75
|
+
updated_at?: string;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
66
78
|
/** Options for building page metadata. */
|
|
67
79
|
export interface BuildPageMetadataOptions {
|
|
68
80
|
siteData?: SiteDataRecord;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SiteNavigationItem } from "./SiteLayout.js";
|
|
2
2
|
import { type BlogIndexEntry, type FilterDimension, type FilterDimensionValues } from "./contentIndex.js";
|
|
3
|
-
import { type TipCurrency
|
|
3
|
+
import { type TipCurrency } from "./tipControls.js";
|
|
4
4
|
import { type PaywallPrice, type PaywallPayClient } from "./paywallControls.js";
|
|
5
5
|
export interface BlogPostSummary extends BlogIndexEntry {
|
|
6
6
|
author?: string;
|
|
@@ -39,14 +39,18 @@ export interface BlogTippingConfig {
|
|
|
39
39
|
enabled: boolean;
|
|
40
40
|
label?: string;
|
|
41
41
|
description?: string;
|
|
42
|
-
recipient
|
|
43
|
-
currencies
|
|
42
|
+
recipient?: string;
|
|
43
|
+
currencies?: TipCurrency[];
|
|
44
44
|
presets?: Record<string, number[]>;
|
|
45
|
-
|
|
45
|
+
dataServerUrl?: string;
|
|
46
46
|
/** When true, `recipientOverride` takes precedence over `recipient`. */
|
|
47
47
|
allowPerPostRecipient?: boolean;
|
|
48
48
|
/** Per-post tip recipient (used when `allowPerPostRecipient` is true). */
|
|
49
49
|
recipientOverride?: string;
|
|
50
|
+
/** Sender wallet address (auto-detected if omitted). */
|
|
51
|
+
senderAddress?: string;
|
|
52
|
+
/** Signs a base64 serialized transaction, returns signed base64. */
|
|
53
|
+
signTransaction?: (serializedTx: string) => Promise<string>;
|
|
50
54
|
}
|
|
51
55
|
export interface BlogPaywallConfig {
|
|
52
56
|
mode: "free" | "preview" | "locked";
|
|
@@ -30,9 +30,9 @@ export function BlogPostTemplate({ siteTitle, navigation, title, bodyMarkdown, b
|
|
|
30
30
|
bodyMarkdown,
|
|
31
31
|
bodyHtml,
|
|
32
32
|
allowUnsafeHtmlFallback
|
|
33
|
-
})] }), tipping?.enabled && (_jsx("section", { className: "cedros-site__card", children: _jsx(TipWidget, { recipient: tipping.allowPerPostRecipient && tipping.recipientOverride
|
|
33
|
+
})] }), tipping?.enabled && (_jsx("section", { className: "cedros-site__card", children: _jsx(TipWidget, { dataServerUrl: tipping.dataServerUrl, recipient: tipping.allowPerPostRecipient && tipping.recipientOverride
|
|
34
34
|
? tipping.recipientOverride
|
|
35
|
-
: tipping.recipient, currencies: tipping.currencies, presets: tipping.presets,
|
|
35
|
+
: tipping.recipient, currencies: tipping.currencies, presets: tipping.presets, label: tipping.label, description: tipping.description, senderAddress: tipping.senderAddress, signTransaction: tipping.signTransaction }) })), relatedPosts.length > 0 && (_jsxs("section", { className: "cedros-site__card", children: [_jsx("h2", { style: { margin: 0, fontSize: "1.02rem" }, children: "Related posts" }), _jsx("div", { className: "cedros-site__content-grid", style: { marginTop: "0.75rem" }, children: relatedPosts.map((post) => (_jsxs("article", { className: "cedros-site__entry-card", children: [_jsx("h3", { className: "cedros-site__entry-title", style: { marginTop: 0 }, children: _jsx("a", { href: `${basePath}/${post.slug}`, children: post.title }) }), post.excerpt && _jsx("p", { className: "cedros-site__subtitle", children: post.excerpt })] }, post.slug))) })] }))] }));
|
|
36
36
|
}
|
|
37
37
|
function BlogIndexControls({ basePath, query, category, tag, sort, categories, tags }) {
|
|
38
38
|
return (_jsxs("form", { method: "get", action: basePath, className: "cedros-site__controls cedros-site__card", children: [_jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Search" }), _jsx("input", { type: "search", name: "q", defaultValue: query, placeholder: "Search blog posts" })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Category" }), _jsxs("select", { name: "category", defaultValue: category, children: [_jsx("option", { value: "", children: "All" }), categories.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Tag" }), _jsxs("select", { name: "tag", defaultValue: tag, children: [_jsx("option", { value: "", children: "All" }), tags.map((entry) => (_jsx("option", { value: entry, children: entry }, entry)))] })] }), _jsxs("label", { className: "cedros-site__control", children: [_jsx("span", { children: "Sort" }), _jsxs("select", { name: "sort", defaultValue: sort, children: [_jsx("option", { value: "newest", children: "Newest" }), _jsx("option", { value: "oldest", children: "Oldest" }), _jsx("option", { value: "title-asc", children: "Title A-Z" }), _jsx("option", { value: "title-desc", children: "Title Z-A" })] })] }), _jsxs("div", { className: "cedros-site__control-actions", children: [_jsx("button", { className: "cedros-site__nav-link", type: "submit", children: "Apply" }), _jsx("a", { className: "cedros-site__nav-link", href: basePath, children: "Clear" })] })] }));
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
background: color-mix(in srgb, var(--cds-link) 9%, var(--cds-muted-bg) 91%);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
.cedros-site__docs-item--depth-0 {
|
|
199
|
+
padding-left: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
198
202
|
.cedros-site__docs-item--depth-1 {
|
|
199
203
|
padding-left: 0.9rem;
|
|
200
204
|
}
|
|
@@ -312,6 +316,10 @@
|
|
|
312
316
|
font-size: 0.8rem;
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
.cedros-site__toc-link--depth-2 {
|
|
320
|
+
padding-left: 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
315
323
|
.cedros-site__toc-link--depth-3 {
|
|
316
324
|
padding-left: 0.65rem;
|
|
317
325
|
}
|
|
@@ -15,14 +15,12 @@ export { Breadcrumbs, ContentPagination, type PaginationProps } from "./contentU
|
|
|
15
15
|
export { HomePageTemplate, type HomePageTemplateProps, type HomeFeature } from "./HomePageTemplate.js";
|
|
16
16
|
export { DocsIndexTemplate, DocArticleTemplate, type DocsIndexTemplateProps, type DocArticleTemplateProps, type DocsIndexItem } from "./DocsTemplates.js";
|
|
17
17
|
export { BlogIndexTemplate, BlogPostTemplate, type BlogIndexTemplateProps, type BlogPostTemplateProps, type BlogPostSummary, type BlogTippingConfig, type BlogPaywallConfig } from "./BlogTemplates.js";
|
|
18
|
-
export { TipWidget } from "./tipControls.js";
|
|
19
|
-
export type { TipWidgetProps, TipCurrency
|
|
18
|
+
export { TipWidget, SOL_CURRENCY, USDC_CURRENCY } from "./tipControls.js";
|
|
19
|
+
export type { TipWidgetProps, TipCurrency } from "./tipControls.js";
|
|
20
20
|
export { ContentPaywall } from "./paywallControls.js";
|
|
21
21
|
export type { ContentPaywallProps, PaywallPrice, PaywallPayClient } from "./paywallControls.js";
|
|
22
22
|
export { BlogSearchInput, FilterDimensionChips, BookmarkButton } from "./blogControls.js";
|
|
23
23
|
export type { BlogSearchInputProps, FilterDimensionChipsProps, BookmarkButtonProps } from "./blogControls.js";
|
|
24
|
-
export { SolanaMicropayments, createSolanaTipClient, detectSolanaWallet, SOL_CURRENCY, USDC_CURRENCY, type SolanaMicropaymentsProps, } from "./solanaMicropayments.js";
|
|
25
|
-
export { ensureRecipientAta } from "./solanaAtaSetup.js";
|
|
26
24
|
export { LegalPageTemplate, type LegalPageTemplateProps } from "./LegalPageTemplate.js";
|
|
27
25
|
export { ContactPageTemplate, type ContactPageTemplateProps, type ContactDetail } from "./ContactPageTemplate.js";
|
|
28
26
|
export { NotFoundTemplate, type NotFoundTemplateProps } from "./NotFoundTemplate.js";
|
|
@@ -15,11 +15,9 @@ export { Breadcrumbs, ContentPagination } from "./contentUi.js";
|
|
|
15
15
|
export { HomePageTemplate } from "./HomePageTemplate.js";
|
|
16
16
|
export { DocsIndexTemplate, DocArticleTemplate } from "./DocsTemplates.js";
|
|
17
17
|
export { BlogIndexTemplate, BlogPostTemplate } from "./BlogTemplates.js";
|
|
18
|
-
export { TipWidget } from "./tipControls.js";
|
|
18
|
+
export { TipWidget, SOL_CURRENCY, USDC_CURRENCY } from "./tipControls.js";
|
|
19
19
|
export { ContentPaywall } from "./paywallControls.js";
|
|
20
20
|
export { BlogSearchInput, FilterDimensionChips, BookmarkButton } from "./blogControls.js";
|
|
21
|
-
export { SolanaMicropayments, createSolanaTipClient, detectSolanaWallet, SOL_CURRENCY, USDC_CURRENCY, } from "./solanaMicropayments.js";
|
|
22
|
-
export { ensureRecipientAta } from "./solanaAtaSetup.js";
|
|
23
21
|
export { LegalPageTemplate } from "./LegalPageTemplate.js";
|
|
24
22
|
export { ContactPageTemplate } from "./ContactPageTemplate.js";
|
|
25
23
|
export { NotFoundTemplate } from "./NotFoundTemplate.js";
|
|
@@ -4,21 +4,38 @@ export interface TipCurrency {
|
|
|
4
4
|
logo: string;
|
|
5
5
|
mint?: string;
|
|
6
6
|
}
|
|
7
|
-
export interface TipParams {
|
|
8
|
-
currency: TipCurrency;
|
|
9
|
-
amount: number;
|
|
10
|
-
recipient: string;
|
|
11
|
-
}
|
|
12
|
-
/** Provided by @cedros/pay-react (or any compatible implementation). */
|
|
13
|
-
export interface TipPayClient {
|
|
14
|
-
sendTip(params: TipParams): Promise<void>;
|
|
15
|
-
}
|
|
16
7
|
export interface TipWidgetProps {
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
/** cedros-data server URL for config fetch; defaults to "" (relative). */
|
|
9
|
+
dataServerUrl?: string;
|
|
10
|
+
/** Tip recipient address. When provided, config fetch is skipped (manual mode). */
|
|
11
|
+
recipient?: string;
|
|
12
|
+
currencies?: TipCurrency[];
|
|
19
13
|
presets?: Record<string, number[]>;
|
|
20
|
-
payClient: TipPayClient;
|
|
21
14
|
label?: string;
|
|
22
15
|
description?: string;
|
|
16
|
+
/** Sender wallet address. Auto-detected from window.solana if omitted. */
|
|
17
|
+
senderAddress?: string;
|
|
18
|
+
/** Signs a base64 serialized transaction, returns signed base64. Uses browser wallet if omitted. */
|
|
19
|
+
signTransaction?: (serializedTx: string) => Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
export declare const SOL_CURRENCY: TipCurrency;
|
|
22
|
+
export declare const USDC_CURRENCY: TipCurrency;
|
|
23
|
+
interface SolanaWallet {
|
|
24
|
+
isConnected: boolean;
|
|
25
|
+
publicKey: {
|
|
26
|
+
toString(): string;
|
|
27
|
+
} | null;
|
|
28
|
+
connect(): Promise<{
|
|
29
|
+
publicKey: {
|
|
30
|
+
toString(): string;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
signTransaction(tx: unknown): Promise<unknown>;
|
|
34
|
+
}
|
|
35
|
+
declare global {
|
|
36
|
+
interface Window {
|
|
37
|
+
solana?: SolanaWallet;
|
|
38
|
+
}
|
|
23
39
|
}
|
|
24
|
-
export declare function TipWidget({ recipient, currencies, presets
|
|
40
|
+
export declare function TipWidget({ dataServerUrl, recipient: recipientProp, currencies: currenciesProp, presets: presetsProp, label: labelProp, description: descriptionProp, senderAddress: senderProp, signTransaction: signProp, }: TipWidgetProps): React.JSX.Element;
|
|
41
|
+
export {};
|
|
@@ -1,7 +1,107 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useState } from "react";
|
|
4
|
-
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Currency constants
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export const SOL_CURRENCY = {
|
|
8
|
+
symbol: "SOL",
|
|
9
|
+
decimals: 9,
|
|
10
|
+
logo: "",
|
|
11
|
+
mint: "So11111111111111111111111111111111111111112",
|
|
12
|
+
};
|
|
13
|
+
export const USDC_CURRENCY = {
|
|
14
|
+
symbol: "USDC",
|
|
15
|
+
decimals: 6,
|
|
16
|
+
logo: "",
|
|
17
|
+
mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
18
|
+
};
|
|
19
|
+
function getWalletAddress() {
|
|
20
|
+
if (typeof window === "undefined")
|
|
21
|
+
return null;
|
|
22
|
+
return window.solana?.publicKey?.toString() ?? null;
|
|
23
|
+
}
|
|
24
|
+
async function connectAndGetAddress() {
|
|
25
|
+
const w = typeof window !== "undefined" ? window.solana : undefined;
|
|
26
|
+
if (!w)
|
|
27
|
+
throw new Error("No Solana wallet detected. Please install Phantom or another wallet.");
|
|
28
|
+
if (!w.isConnected) {
|
|
29
|
+
const { publicKey } = await w.connect();
|
|
30
|
+
return publicKey.toString();
|
|
31
|
+
}
|
|
32
|
+
if (!w.publicKey)
|
|
33
|
+
throw new Error("Wallet connected but no public key available.");
|
|
34
|
+
return w.publicKey.toString();
|
|
35
|
+
}
|
|
36
|
+
/** Signs base64 tx via browser wallet. Requires @solana/web3.js at runtime. */
|
|
37
|
+
async function signWithBrowserWallet(serializedTx) {
|
|
38
|
+
const wallet = window.solana;
|
|
39
|
+
if (!wallet)
|
|
40
|
+
throw new Error("No Solana wallet detected");
|
|
41
|
+
if (!wallet.isConnected)
|
|
42
|
+
await wallet.connect();
|
|
43
|
+
let Transaction;
|
|
44
|
+
try {
|
|
45
|
+
({ Transaction } = await import("@solana/web3.js"));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("Browser wallet signing requires @solana/web3.js. " +
|
|
49
|
+
"Install it or provide a signTransaction prop.");
|
|
50
|
+
}
|
|
51
|
+
const txBytes = Uint8Array.from(atob(serializedTx), (c) => c.charCodeAt(0));
|
|
52
|
+
const tx = Transaction.from(txBytes);
|
|
53
|
+
const signed = (await wallet.signTransaction(tx));
|
|
54
|
+
const signedBytes = signed.serialize();
|
|
55
|
+
return btoa(String.fromCharCode(...signedBytes));
|
|
56
|
+
}
|
|
57
|
+
function useTipConfig(dataServerUrl, skip) {
|
|
58
|
+
const [config, setConfig] = useState(null);
|
|
59
|
+
const [loading, setLoading] = useState(!skip);
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (skip)
|
|
63
|
+
return;
|
|
64
|
+
let cancelled = false;
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${dataServerUrl}/site/config/tipping`);
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`Tipping config: ${res.status}`);
|
|
70
|
+
const data = (await res.json());
|
|
71
|
+
if (!cancelled) {
|
|
72
|
+
setConfig(data);
|
|
73
|
+
setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
return () => { cancelled = true; };
|
|
84
|
+
}, [dataServerUrl, skip]);
|
|
85
|
+
return { config, loading, error };
|
|
86
|
+
}
|
|
87
|
+
export function TipWidget({ dataServerUrl = "", recipient: recipientProp, currencies: currenciesProp, presets: presetsProp, label: labelProp, description: descriptionProp, senderAddress: senderProp, signTransaction: signProp, }) {
|
|
88
|
+
const manualMode = !!recipientProp;
|
|
89
|
+
const { config, loading: configLoading } = useTipConfig(dataServerUrl, manualMode);
|
|
90
|
+
const recipient = recipientProp ?? config?.recipient ?? "";
|
|
91
|
+
const currencies = currenciesProp ?? config?.currencies ?? [SOL_CURRENCY];
|
|
92
|
+
const presets = presetsProp ?? config?.presets;
|
|
93
|
+
const label = labelProp ?? config?.label ?? "Leave a tip";
|
|
94
|
+
const description = descriptionProp ?? config?.description;
|
|
95
|
+
const [walletAddress, setWalletAddress] = useState(senderProp ?? null);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (senderProp) {
|
|
98
|
+
setWalletAddress(senderProp);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const detected = getWalletAddress();
|
|
102
|
+
if (detected)
|
|
103
|
+
setWalletAddress(detected);
|
|
104
|
+
}, [senderProp]);
|
|
5
105
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
6
106
|
const [amount, setAmount] = useState("");
|
|
7
107
|
const [status, setStatus] = useState("idle");
|
|
@@ -9,7 +109,7 @@ export function TipWidget({ recipient, currencies, presets, payClient, label = "
|
|
|
9
109
|
const selectedCurrency = currencies[selectedIndex] ?? currencies[0];
|
|
10
110
|
const currentPresets = selectedCurrency ? presets?.[selectedCurrency.symbol] : undefined;
|
|
11
111
|
const handleSubmit = useCallback(async () => {
|
|
12
|
-
if (!selectedCurrency)
|
|
112
|
+
if (!selectedCurrency || !recipient)
|
|
13
113
|
return;
|
|
14
114
|
const numericAmount = parseFloat(amount);
|
|
15
115
|
if (!numericAmount || numericAmount <= 0)
|
|
@@ -17,27 +117,41 @@ export function TipWidget({ recipient, currencies, presets, payClient, label = "
|
|
|
17
117
|
setStatus("loading");
|
|
18
118
|
setErrorMessage("");
|
|
19
119
|
try {
|
|
20
|
-
|
|
120
|
+
let sender = walletAddress;
|
|
121
|
+
if (!sender) {
|
|
122
|
+
sender = await connectAndGetAddress();
|
|
123
|
+
setWalletAddress(sender);
|
|
124
|
+
}
|
|
125
|
+
const mint = selectedCurrency.mint ?? SOL_CURRENCY.mint;
|
|
126
|
+
const rawAmount = String(Math.round(numericAmount * 10 ** selectedCurrency.decimals));
|
|
127
|
+
let TradeApiClient;
|
|
128
|
+
try {
|
|
129
|
+
({ TradeApiClient } = await import("@cedros/trade-react"));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error("TipWidget requires @cedros/trade-react. Install it as a dependency.");
|
|
133
|
+
}
|
|
134
|
+
const client = new TradeApiClient(dataServerUrl);
|
|
135
|
+
const buildResult = await client.buildTransfer({ sender, recipient, mint, amount: rawAmount });
|
|
136
|
+
const sign = signProp ?? signWithBrowserWallet;
|
|
137
|
+
const signedTx = await sign(buildResult.transaction);
|
|
138
|
+
const result = await client.executeTransfer(signedTx);
|
|
139
|
+
if (result.status === "failed")
|
|
140
|
+
throw new Error("Transaction failed on-chain");
|
|
21
141
|
setStatus("success");
|
|
22
142
|
setAmount("");
|
|
23
143
|
}
|
|
24
144
|
catch (err) {
|
|
25
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
26
145
|
setStatus("error");
|
|
27
|
-
setErrorMessage(
|
|
146
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
28
147
|
}
|
|
29
|
-
}, [selectedCurrency, amount,
|
|
30
|
-
|
|
148
|
+
}, [selectedCurrency, amount, recipient, walletAddress, signProp, dataServerUrl]);
|
|
149
|
+
// Early returns: loading, disabled, or no recipient
|
|
150
|
+
if (!manualMode && configLoading)
|
|
151
|
+
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
152
|
+
if (!manualMode && config && !config.enabled)
|
|
153
|
+
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
154
|
+
if (!recipient || !selectedCurrency)
|
|
31
155
|
return _jsx("div", { className: "cedros-site__tip-widget" });
|
|
32
|
-
return (_jsxs("div", { className: "cedros-site__tip-widget", children: [_jsx("h3", { className: "cedros-site__tip-title", children: label }), description && _jsx("p", { className: "cedros-site__tip-description", children: description }), currencies.length > 1 && (_jsx("select", { className: "cedros-site__tip-currency", value: selectedIndex, onChange: (e) => {
|
|
33
|
-
setSelectedIndex(Number(e.target.value));
|
|
34
|
-
setAmount("");
|
|
35
|
-
setStatus("idle");
|
|
36
|
-
}, children: currencies.map((c, i) => (_jsx("option", { value: i, children: c.symbol }, c.symbol))) })), currentPresets && currentPresets.length > 0 && (_jsx("div", { className: "cedros-site__tip-presets", children: currentPresets.map((presetAmount) => (_jsxs("button", { type: "button", className: "cedros-site__pill", onClick: () => {
|
|
37
|
-
setAmount(String(presetAmount));
|
|
38
|
-
setStatus("idle");
|
|
39
|
-
}, children: [presetAmount, " ", selectedCurrency.symbol] }, presetAmount))) })), _jsx("div", { className: "cedros-site__tip-amount", children: _jsx("input", { type: "number", min: "0", step: "any", placeholder: `Amount in ${selectedCurrency.symbol}`, value: amount, onChange: (e) => {
|
|
40
|
-
setAmount(e.target.value);
|
|
41
|
-
setStatus("idle");
|
|
42
|
-
} }) }), _jsx("button", { type: "button", className: "cedros-site__tip-submit", disabled: status === "loading" || !amount || parseFloat(amount) <= 0, onClick: () => void handleSubmit(), children: status === "loading" ? "Sending..." : "Send Tip" }), status === "success" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--success", children: "Tip sent successfully!" })), status === "error" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--error", children: errorMessage || "Failed to send tip." }))] }));
|
|
156
|
+
return (_jsxs("div", { className: "cedros-site__tip-widget", children: [_jsx("h3", { className: "cedros-site__tip-title", children: label }), description && _jsx("p", { className: "cedros-site__tip-description", children: description }), currencies.length > 1 && (_jsx("select", { className: "cedros-site__tip-currency", value: selectedIndex, onChange: (e) => { setSelectedIndex(Number(e.target.value)); setAmount(""); setStatus("idle"); }, children: currencies.map((c, i) => (_jsx("option", { value: i, children: c.symbol }, c.symbol))) })), currentPresets && currentPresets.length > 0 && (_jsx("div", { className: "cedros-site__tip-presets", children: currentPresets.map((presetAmount) => (_jsxs("button", { type: "button", className: "cedros-site__pill", onClick: () => { setAmount(String(presetAmount)); setStatus("idle"); }, children: [presetAmount, " ", selectedCurrency.symbol] }, presetAmount))) })), _jsx("div", { className: "cedros-site__tip-amount", children: _jsx("input", { type: "number", min: "0", step: "any", placeholder: `Amount in ${selectedCurrency.symbol}`, value: amount, onChange: (e) => { setAmount(e.target.value); setStatus("idle"); } }) }), _jsx("button", { type: "button", className: "cedros-site__tip-submit", disabled: status === "loading" || !amount || parseFloat(amount) <= 0, onClick: () => void handleSubmit(), children: status === "loading" ? "Sending..." : "Send Tip" }), status === "success" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--success", children: "Tip sent successfully!" })), status === "error" && (_jsx("p", { className: "cedros-site__tip-status cedros-site__tip-status--error", children: errorMessage || "Failed to send tip." }))] }));
|
|
43
157
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedros/data-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "React components, page templates, and Next.js integration for cedros-data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react/index.js",
|
|
@@ -40,11 +40,15 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
43
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"@cedros/trade-react": "^0.1.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@cedros/trade-react": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
|
-
"@solana/spl-token": "^0.4.0",
|
|
47
|
-
"@solana/web3.js": "^1.98.4",
|
|
48
52
|
"highlight.js": "^11.11.1",
|
|
49
53
|
"react-markdown": "^10.1.0",
|
|
50
54
|
"rehype-autolink-headings": "^7.1.0",
|