dh-remixer-sdk 0.0.28-cd8a4a8 → 0.0.28-e1df2dd
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/package.json +1 -1
- package/scripts/sdk-update/cmd-utils.mjs +22 -4
- package/scripts/sdk-update/sdk-utils.mjs +13 -0
- package/scripts/ssg-helmet/prerenderer.mjs +9 -0
- package/templates/base/SEOHead.tsx +43 -0
- package/templates/base/filemap.json +10 -1
- package/templates/base/index.tsx +7 -4
- package/templates/base/package.json +2 -2
- package/templates/base/remixer/actions.ts +5 -0
- package/templates/base/remixer/auth.ts +21 -0
- package/templates/base/remixer/core.ts +49 -0
- package/templates/base/remixer/data.ts +223 -0
- package/templates/base/remixer/ecommerce.ts +179 -0
- package/templates/base/remixer/runtime.ts +131 -0
- package/templates/base/remixer/storage.ts +11 -0
- package/templates/base/remixer.ts +49 -0
- package/templates/base/supabase.ts +21 -10
- package/templates/base/vite.config.ts +42 -148
- package/templates/ecommerce/shipping.ts +11 -59
- package/templates/ecommerce/stripe-checkout.ts +16 -53
- package/templates/ecommerce/supabase.ts +21 -10
- package/templates/immersive/Anchor3D.tsx +62 -0
- package/templates/immersive/Atmosphere.tsx +134 -0
- package/templates/immersive/ImmersiveStory.tsx +130 -0
- package/templates/immersive/ScrollStory.tsx +136 -0
- package/templates/immersive/Stage3D.tsx +194 -0
- package/templates/immersive/StageContext.ts +37 -0
- package/templates/immersive/anchors.tsx +77 -0
- package/templates/immersive/cleanup.json +22 -0
- package/templates/immersive/filemap.json +13 -0
- package/templates/immersive/package.json +13 -0
- package/templates/immersive/phases.ts +154 -0
- package/templates/immersive/safeCanvas.tsx +82 -0
- package/templates/immersive/stage.ts +5 -0
- package/templates/immersive/useAnchor.ts +13 -0
- package/templates/landing-page/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { TARGET_ENVIRONMENT } from "./vars.mjs";
|
|
3
3
|
|
|
4
|
+
const TAG_BY_ENV = {
|
|
5
|
+
production: "latest",
|
|
6
|
+
staging: "staging",
|
|
7
|
+
develop: "develop",
|
|
8
|
+
};
|
|
9
|
+
|
|
4
10
|
export async function getSdkVersion() {
|
|
11
|
+
const tag = TAG_BY_ENV[TARGET_ENVIRONMENT];
|
|
12
|
+
|
|
13
|
+
if (!tag) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`[sdk-update] Unknown NEXT_PUBLIC_ENVIRONMENT "${TARGET_ENVIRONMENT}". ` +
|
|
16
|
+
`Expected one of: ${Object.keys(TAG_BY_ENV).join(", ")}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
const res = await fetch("https://registry.npmjs.org/dh-remixer-sdk");
|
|
6
21
|
const data = await res.json();
|
|
7
|
-
const sdkVersion =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
const sdkVersion = data["dist-tags"]?.[tag];
|
|
23
|
+
|
|
24
|
+
if (!sdkVersion) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`[sdk-update] No "${tag}" dist-tag published for dh-remixer-sdk on npm`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
11
29
|
|
|
12
30
|
return sdkVersion;
|
|
13
31
|
}
|
|
@@ -5,7 +5,19 @@ import { SDK_ROOT } from "./vars.mjs";
|
|
|
5
5
|
import { jsonMerge } from "./type-utils.mjs";
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
function assertTemplateExists(templateType) {
|
|
9
|
+
if (templateType === "base") return;
|
|
10
|
+
const templateRoot = path.join(SDK_ROOT, "templates", templateType);
|
|
11
|
+
if (!fssync.existsSync(templateRoot)) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`[sdk-update] Template "${templateType}" not found in installed dh-remixer-sdk (${templateRoot}). ` +
|
|
14
|
+
`Run: npm install dh-remixer-sdk@latest`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export async function getDeletionList(templateType) {
|
|
20
|
+
assertTemplateExists(templateType);
|
|
9
21
|
const baseRoot = path.join(SDK_ROOT, "templates", "base");
|
|
10
22
|
const templateRoot = path.join(SDK_ROOT, "templates", templateType);
|
|
11
23
|
const templateCleanupPath = path.join(templateRoot, "cleanup.json");
|
|
@@ -23,6 +35,7 @@ export async function getDeletionList(templateType) {
|
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export async function getExtractionList(templateType) {
|
|
38
|
+
assertTemplateExists(templateType);
|
|
26
39
|
const baseRoot = path.join(SDK_ROOT, "templates", "base");
|
|
27
40
|
const templateRoot = path.join(SDK_ROOT, "templates", templateType);
|
|
28
41
|
const templateFilemapPath = path.join(templateRoot, "filemap.json");
|
|
@@ -202,6 +202,15 @@ export class Prerenderer {
|
|
|
202
202
|
this.#client = await CDP({ port: this.#cdpPort, target: pageTarget });
|
|
203
203
|
await this.#client.Page.enable();
|
|
204
204
|
await this.#client.Runtime.enable();
|
|
205
|
+
|
|
206
|
+
// Signal to the app that we are in SSG prerender mode, BEFORE any page
|
|
207
|
+
// scripts run. Components (e.g. ScrollScene, useScrollProgress) can read
|
|
208
|
+
// window.__PRERENDER__ to skip WebGL/Lenis and render a static fallback,
|
|
209
|
+
// which keeps headless Chrome from hanging on canvas rendering and
|
|
210
|
+
// guarantees meaningful HTML for SEO.
|
|
211
|
+
await this.#client.Page.addScriptToEvaluateOnNewDocument({
|
|
212
|
+
source: "window.__PRERENDER__ = true;",
|
|
213
|
+
});
|
|
205
214
|
}
|
|
206
215
|
|
|
207
216
|
async #launchDirect() {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Helmet } from "react-helmet-async";
|
|
2
|
+
import { useLanguage } from "@/context/LanguageContext";
|
|
3
|
+
import images from "@/assets/images.json";
|
|
4
|
+
|
|
5
|
+
interface SEOHeadProps {
|
|
6
|
+
titleKey?: string;
|
|
7
|
+
descriptionKey?: string;
|
|
8
|
+
ogImage?: string;
|
|
9
|
+
ogImageKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Canonical <head> for every page. Render exactly one <SEOHead /> per page
|
|
13
|
+
// inside pages/*.tsx; no raw <Helmet> anywhere else. Pulls og:image from
|
|
14
|
+
// assets/images.json by default so social sharing works without extra wiring.
|
|
15
|
+
export function SEOHead({
|
|
16
|
+
titleKey = "home.title",
|
|
17
|
+
descriptionKey = "home.description",
|
|
18
|
+
ogImage,
|
|
19
|
+
ogImageKey = "og_image",
|
|
20
|
+
}: SEOHeadProps) {
|
|
21
|
+
const { t } = useLanguage();
|
|
22
|
+
const siteTitle = import.meta.env.VITE_METADATA_TITLE as string | undefined;
|
|
23
|
+
const translated = t(titleKey);
|
|
24
|
+
const title = siteTitle ? `${translated} | ${siteTitle}` : translated;
|
|
25
|
+
const description = t(descriptionKey);
|
|
26
|
+
const resolvedOg =
|
|
27
|
+
ogImage ?? (images as Record<string, string>)[ogImageKey];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Helmet>
|
|
31
|
+
<title>{title}</title>
|
|
32
|
+
<meta name="description" content={description} />
|
|
33
|
+
<meta property="og:title" content={title} />
|
|
34
|
+
<meta property="og:description" content={description} />
|
|
35
|
+
<meta property="og:type" content="website" />
|
|
36
|
+
{resolvedOg ? <meta property="og:image" content={resolvedOg} /> : null}
|
|
37
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
38
|
+
{resolvedOg ? <meta name="twitter:image" content={resolvedOg} /> : null}
|
|
39
|
+
</Helmet>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default SEOHead;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"index.tsx": "index.tsx",
|
|
3
|
+
"SEOHead.tsx": "SEOHead.tsx",
|
|
3
4
|
"gitignore": ".gitignore",
|
|
4
5
|
"index.html": "index.html",
|
|
5
6
|
"package.json": "package.json",
|
|
@@ -8,5 +9,13 @@
|
|
|
8
9
|
"vite.config.ts": "vite.config.ts",
|
|
9
10
|
"robots.txt": "public/robots.txt",
|
|
10
11
|
".htaccess": "public/.htaccess",
|
|
11
|
-
"supabase.ts": "lib/supabase.ts"
|
|
12
|
+
"supabase.ts": "lib/supabase.ts",
|
|
13
|
+
"remixer.ts": "lib/remixer.ts",
|
|
14
|
+
"remixer/actions.ts": "lib/remixer/actions.ts",
|
|
15
|
+
"remixer/auth.ts": "lib/remixer/auth.ts",
|
|
16
|
+
"remixer/core.ts": "lib/remixer/core.ts",
|
|
17
|
+
"remixer/data.ts": "lib/remixer/data.ts",
|
|
18
|
+
"remixer/ecommerce.ts": "lib/remixer/ecommerce.ts",
|
|
19
|
+
"remixer/runtime.ts": "lib/remixer/runtime.ts",
|
|
20
|
+
"remixer/storage.ts": "lib/remixer/storage.ts"
|
|
12
21
|
}
|
package/templates/base/index.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import ReactDOM from "react-dom/client";
|
|
3
|
+
import { HelmetProvider } from "react-helmet-async";
|
|
3
4
|
import tailwindConfig from "@/tailwind.config";
|
|
4
5
|
import App from "@/App";
|
|
5
6
|
import { GOOGLE_FONTS_URL } from "@/fonts";
|
|
@@ -27,8 +28,10 @@ if (!rootElement) {
|
|
|
27
28
|
const root = ReactDOM.createRoot(rootElement);
|
|
28
29
|
root.render(
|
|
29
30
|
<React.StrictMode>
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
31
|
+
<HelmetProvider>
|
|
32
|
+
<LanguageProvider>
|
|
33
|
+
<App />
|
|
34
|
+
</LanguageProvider>
|
|
35
|
+
</HelmetProvider>
|
|
33
36
|
</React.StrictMode>
|
|
34
|
-
);
|
|
37
|
+
);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"start": "vite preview --host 0.0.0.0 --mode production --port ${PORT:-4173}",
|
|
9
9
|
"dev": "vite --host 0.0.0.0 --port ${PORT:-4173} --mode development",
|
|
10
10
|
"lint": "echo 'Linting not implemented!'",
|
|
11
|
-
"remixer-sdk:update": "
|
|
11
|
+
"remixer-sdk:update": "bun install dh-remixer-sdk@${REMIXER_SDK_TAG:-latest} && sdk-update"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@openobserve/browser-logs": "0.3.1",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"react": "19.2.0",
|
|
22
22
|
"react-aria-components": "1.11.0",
|
|
23
23
|
"react-dom": "19.2.0",
|
|
24
|
-
"react-helmet": "
|
|
24
|
+
"react-helmet-async": "3.0.0",
|
|
25
25
|
"react-router-dom": "7.9.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getProjectSession, isValidProjectSession, supabase } from '../supabase';
|
|
2
|
+
|
|
3
|
+
export const auth = {
|
|
4
|
+
getSession: getProjectSession,
|
|
5
|
+
async getUser() {
|
|
6
|
+
return (await getProjectSession())?.user ?? null;
|
|
7
|
+
},
|
|
8
|
+
isValidProjectSession,
|
|
9
|
+
onAuthStateChange(callback: Parameters<typeof supabase.auth.onAuthStateChange>[0]) {
|
|
10
|
+
return supabase.auth.onAuthStateChange(callback);
|
|
11
|
+
},
|
|
12
|
+
setSession(accessToken: string, refreshToken: string) {
|
|
13
|
+
return supabase.auth.setSession({
|
|
14
|
+
access_token: accessToken,
|
|
15
|
+
refresh_token: refreshToken,
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
signOut() {
|
|
19
|
+
return supabase.auth.signOut();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { authHeader } from '../supabase';
|
|
2
|
+
|
|
3
|
+
export type RemixerRecordData = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export async function runtimeDataFetch<T>(path: string, body: RemixerRecordData): Promise<T> {
|
|
6
|
+
const headers = await authHeader();
|
|
7
|
+
const response = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/${path}`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
...headers,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(body),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const error = await response.json().catch(() => ({}));
|
|
18
|
+
throw new Error(error.message || error.error || `Remixer data request failed (${response.status})`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return response.json();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function remixerFunctionFetch<T>(
|
|
25
|
+
path: string,
|
|
26
|
+
options: { method?: 'GET' | 'POST'; body?: RemixerRecordData; query?: Record<string, string | number | boolean | undefined> } = {},
|
|
27
|
+
): Promise<T> {
|
|
28
|
+
const headers = await authHeader();
|
|
29
|
+
const url = new URL(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/${path}`);
|
|
30
|
+
Object.entries(options.query || {}).forEach(([key, value]) => {
|
|
31
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const response = await fetch(url.toString(), {
|
|
35
|
+
method: options.method || 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
...headers,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
},
|
|
40
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const error = await response.json().catch(() => ({}));
|
|
45
|
+
throw new Error(error.message || error.error || `Remixer request failed (${response.status})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { supabase } from '../supabase';
|
|
2
|
+
import { runtimeDataFetch } from './core';
|
|
3
|
+
import type { RemixerRecordData } from './core';
|
|
4
|
+
|
|
5
|
+
export type { RemixerRecordData };
|
|
6
|
+
|
|
7
|
+
export type RemixerDataRecord<TData extends RemixerRecordData = RemixerRecordData> = {
|
|
8
|
+
id: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
entityType: string;
|
|
11
|
+
userId?: string | null;
|
|
12
|
+
data: TData;
|
|
13
|
+
status?: string | null;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type RemixerDataField = 'id' | 'entityType' | 'userId' | 'status' | 'createdAt' | 'updatedAt' | `data.${string}`;
|
|
19
|
+
|
|
20
|
+
export type RemixerDataFilterOp = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'contains' | 'startsWith' | 'ilike' | 'isNull';
|
|
21
|
+
|
|
22
|
+
export type RemixerDataFilter = {
|
|
23
|
+
field: RemixerDataField;
|
|
24
|
+
op: RemixerDataFilterOp;
|
|
25
|
+
type?: 'text' | 'number' | 'date' | 'boolean';
|
|
26
|
+
value?: string | number | boolean | null | Array<string | number | boolean>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RemixerDataOrFilter = {
|
|
30
|
+
or: RemixerDataFilter[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type RemixerDataFilterInput = RemixerDataFilter | RemixerDataOrFilter;
|
|
34
|
+
|
|
35
|
+
export type RemixerDataOrder = {
|
|
36
|
+
field: RemixerDataField;
|
|
37
|
+
direction?: 'asc' | 'desc';
|
|
38
|
+
type?: 'text' | 'number' | 'date' | 'boolean';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type RemixerDataRange = {
|
|
42
|
+
from: number;
|
|
43
|
+
to: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type RemixerDataCount = 'exact' | 'planned' | 'estimated';
|
|
47
|
+
|
|
48
|
+
export type RemixerDataListOptions = {
|
|
49
|
+
limit?: number;
|
|
50
|
+
range?: RemixerDataRange;
|
|
51
|
+
filters?: RemixerDataFilterInput[];
|
|
52
|
+
orderBy?: RemixerDataOrder | RemixerDataOrder[];
|
|
53
|
+
count?: RemixerDataCount;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type RemixerDataMaybeOneOptions = {
|
|
57
|
+
id?: string;
|
|
58
|
+
filters?: RemixerDataFilterInput[];
|
|
59
|
+
orderBy?: RemixerDataOrder | RemixerDataOrder[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type RemixerDataMutationOptions = {
|
|
63
|
+
status?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type RemixerDataRecordPage<TData extends RemixerRecordData = RemixerRecordData> = {
|
|
67
|
+
records: RemixerDataRecord<TData>[];
|
|
68
|
+
count?: number | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function mapRealtimeRecord<TData extends RemixerRecordData = RemixerRecordData>(row: Record<string, any>) {
|
|
72
|
+
return {
|
|
73
|
+
id: row.id,
|
|
74
|
+
projectId: row.project_id,
|
|
75
|
+
entityType: row.entity_type,
|
|
76
|
+
userId: row.user_id,
|
|
77
|
+
data: row.data || {},
|
|
78
|
+
status: row.status,
|
|
79
|
+
createdAt: row.created_at,
|
|
80
|
+
updatedAt: row.updated_at,
|
|
81
|
+
} as RemixerDataRecord<TData>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function dataFieldValue(record: RemixerDataRecord, field: RemixerDataField) {
|
|
85
|
+
if (field.startsWith('data.')) return record.data[field.slice('data.'.length)];
|
|
86
|
+
return record[field as keyof RemixerDataRecord];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function matchesIlike(value: unknown, pattern: string) {
|
|
90
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
const regex = new RegExp(`^${escaped.replace(/%/g, '.*').replace(/_/g, '.')}$`, 'i');
|
|
92
|
+
return regex.test(String(value ?? ''));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchesDataFilter(record: RemixerDataRecord, filter: RemixerDataFilter) {
|
|
96
|
+
const value = dataFieldValue(record, filter.field);
|
|
97
|
+
|
|
98
|
+
if (filter.op === 'isNull') return value === null || value === undefined;
|
|
99
|
+
if (filter.value === undefined || filter.value === null) return false;
|
|
100
|
+
if (filter.op === 'in') return Array.isArray(filter.value) && filter.value.map(String).includes(String(value));
|
|
101
|
+
if (filter.op === 'contains') return String(value ?? '').toLowerCase().includes(String(filter.value).toLowerCase());
|
|
102
|
+
if (filter.op === 'startsWith') return String(value ?? '').toLowerCase().startsWith(String(filter.value).toLowerCase());
|
|
103
|
+
if (filter.op === 'ilike') return matchesIlike(value, String(filter.value));
|
|
104
|
+
if (filter.type === 'number') {
|
|
105
|
+
const left = Number(value);
|
|
106
|
+
const right = Number(filter.value);
|
|
107
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return false;
|
|
108
|
+
if (filter.op === 'neq') return left !== right;
|
|
109
|
+
if (filter.op === 'lt') return left < right;
|
|
110
|
+
if (filter.op === 'lte') return left <= right;
|
|
111
|
+
if (filter.op === 'gt') return left > right;
|
|
112
|
+
if (filter.op === 'gte') return left >= right;
|
|
113
|
+
return left === right;
|
|
114
|
+
}
|
|
115
|
+
if (filter.type === 'boolean') {
|
|
116
|
+
const left = value === true || value === 'true';
|
|
117
|
+
const right = filter.value === true || filter.value === 'true';
|
|
118
|
+
return filter.op === 'neq' ? left !== right : left === right;
|
|
119
|
+
}
|
|
120
|
+
if (filter.op === 'neq') return String(value) !== String(filter.value);
|
|
121
|
+
if (filter.op === 'lt') return String(value) < String(filter.value);
|
|
122
|
+
if (filter.op === 'lte') return String(value) <= String(filter.value);
|
|
123
|
+
if (filter.op === 'gt') return String(value) > String(filter.value);
|
|
124
|
+
if (filter.op === 'gte') return String(value) >= String(filter.value);
|
|
125
|
+
|
|
126
|
+
return String(value) === String(filter.value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function matchesDataFilterInput(record: RemixerDataRecord, filter: RemixerDataFilterInput) {
|
|
130
|
+
if ('or' in filter) return filter.or.some(item => matchesDataFilter(record, item));
|
|
131
|
+
return matchesDataFilter(record, filter);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function matchesRealtimeFilters(record: RemixerDataRecord, options: RemixerDataListOptions = {}) {
|
|
135
|
+
return (options.filters || []).every(filter => matchesDataFilterInput(record, filter));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const data = {
|
|
139
|
+
list<TData extends RemixerRecordData = RemixerRecordData>(
|
|
140
|
+
entityType: string,
|
|
141
|
+
options: RemixerDataListOptions = {},
|
|
142
|
+
) {
|
|
143
|
+
return runtimeDataFetch<RemixerDataRecordPage<TData>>('remixer-runtime/data/records/list', {
|
|
144
|
+
entityType,
|
|
145
|
+
...options,
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
get<TData extends RemixerRecordData = RemixerRecordData>(entityType: string, id: string) {
|
|
150
|
+
return runtimeDataFetch<RemixerDataRecord<TData>>('remixer-runtime/data/records/get', {
|
|
151
|
+
entityType,
|
|
152
|
+
id,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
maybeOne<TData extends RemixerRecordData = RemixerRecordData>(
|
|
157
|
+
entityType: string,
|
|
158
|
+
options: RemixerDataMaybeOneOptions = {},
|
|
159
|
+
) {
|
|
160
|
+
return runtimeDataFetch<RemixerDataRecord<TData> | null>('remixer-runtime/data/records/get', {
|
|
161
|
+
entityType,
|
|
162
|
+
...options,
|
|
163
|
+
maybe: true,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
create<TData extends RemixerRecordData = RemixerRecordData>(
|
|
168
|
+
entityType: string,
|
|
169
|
+
data: TData,
|
|
170
|
+
options: RemixerDataMutationOptions = {},
|
|
171
|
+
) {
|
|
172
|
+
return runtimeDataFetch<RemixerDataRecord<TData>>('remixer-runtime/data/records/create', {
|
|
173
|
+
entityType,
|
|
174
|
+
data,
|
|
175
|
+
...options,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
update<TData extends RemixerRecordData = RemixerRecordData>(
|
|
180
|
+
entityType: string,
|
|
181
|
+
id: string,
|
|
182
|
+
data: Partial<TData>,
|
|
183
|
+
options: RemixerDataMutationOptions = {},
|
|
184
|
+
) {
|
|
185
|
+
return runtimeDataFetch<RemixerDataRecord<TData>>('remixer-runtime/data/records/update', {
|
|
186
|
+
entityType,
|
|
187
|
+
id,
|
|
188
|
+
data,
|
|
189
|
+
...options,
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
delete<TData extends RemixerRecordData = RemixerRecordData>(entityType: string, id: string) {
|
|
194
|
+
return runtimeDataFetch<RemixerDataRecord<TData>>('remixer-runtime/data/records/delete', {
|
|
195
|
+
entityType,
|
|
196
|
+
id,
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
subscribe<TData extends RemixerRecordData = RemixerRecordData>(
|
|
201
|
+
entityType: string,
|
|
202
|
+
callback: (record: RemixerDataRecord<TData>) => void,
|
|
203
|
+
options: RemixerDataListOptions = {},
|
|
204
|
+
) {
|
|
205
|
+
const channel = supabase
|
|
206
|
+
.channel(`remixer-data-${entityType}-${crypto.randomUUID()}`)
|
|
207
|
+
.on('postgres_changes', {
|
|
208
|
+
event: '*',
|
|
209
|
+
schema: 'public',
|
|
210
|
+
table: 'entity_records',
|
|
211
|
+
filter: `entity_type=eq.${entityType}`,
|
|
212
|
+
}, (payload) => {
|
|
213
|
+
const row = (payload.new && Object.keys(payload.new).length ? payload.new : payload.old) as Record<string, any>;
|
|
214
|
+
const record = mapRealtimeRecord<TData>(row);
|
|
215
|
+
if (matchesRealtimeFilters(record, options)) callback(record);
|
|
216
|
+
})
|
|
217
|
+
.subscribe();
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
supabase.removeChannel(channel);
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { runtimeDataFetch } from './core';
|
|
2
|
+
import type { RemixerRecordData } from './core';
|
|
3
|
+
|
|
4
|
+
export type RemixerCheckoutItem = {
|
|
5
|
+
priceId: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RemixerShippingAddress = {
|
|
10
|
+
name?: string;
|
|
11
|
+
company?: string;
|
|
12
|
+
street1: string;
|
|
13
|
+
street2?: string;
|
|
14
|
+
city: string;
|
|
15
|
+
state?: string;
|
|
16
|
+
zip: string;
|
|
17
|
+
country: string;
|
|
18
|
+
phone?: string;
|
|
19
|
+
email?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RemixerCheckoutParams = {
|
|
23
|
+
items: RemixerCheckoutItem[];
|
|
24
|
+
mode: 'payment' | 'subscription';
|
|
25
|
+
successUrl: string;
|
|
26
|
+
cancelUrl: string;
|
|
27
|
+
guestEmail?: string;
|
|
28
|
+
shippingRateId?: string;
|
|
29
|
+
shippingCost?: number;
|
|
30
|
+
shippingLabel?: string;
|
|
31
|
+
shippingAddress?: RemixerShippingAddress;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RemixerCheckoutSession = {
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
url: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type RemixerStripeOrderStatus = 'pending' | 'completed' | 'canceled';
|
|
40
|
+
|
|
41
|
+
export type RemixerStripeOrder = {
|
|
42
|
+
id: number;
|
|
43
|
+
customer_id: string;
|
|
44
|
+
checkout_session_id: string;
|
|
45
|
+
payment_intent_id: string | null;
|
|
46
|
+
amount_subtotal: number | null;
|
|
47
|
+
amount_total: number | null;
|
|
48
|
+
currency: string | null;
|
|
49
|
+
payment_status: string | null;
|
|
50
|
+
status: RemixerStripeOrderStatus | null;
|
|
51
|
+
shippo_order_id: string | null;
|
|
52
|
+
tracking_number: string | null;
|
|
53
|
+
tracking_url: string | null;
|
|
54
|
+
carrier: string | null;
|
|
55
|
+
tracking_status: string | null;
|
|
56
|
+
created_at: string;
|
|
57
|
+
updated_at: string | null;
|
|
58
|
+
deleted_at: string | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type RemixerStripeSubscriptionStatus =
|
|
62
|
+
| 'not_started'
|
|
63
|
+
| 'incomplete'
|
|
64
|
+
| 'incomplete_expired'
|
|
65
|
+
| 'trialing'
|
|
66
|
+
| 'active'
|
|
67
|
+
| 'past_due'
|
|
68
|
+
| 'canceled'
|
|
69
|
+
| 'unpaid'
|
|
70
|
+
| 'paused';
|
|
71
|
+
|
|
72
|
+
export type RemixerStripeSubscription = {
|
|
73
|
+
subscription_id: string | null;
|
|
74
|
+
status: RemixerStripeSubscriptionStatus;
|
|
75
|
+
price_id: string | null;
|
|
76
|
+
current_period_start: number | null;
|
|
77
|
+
current_period_end: number | null;
|
|
78
|
+
cancel_at_period_end: boolean | null;
|
|
79
|
+
payment_method_brand: string | null;
|
|
80
|
+
payment_method_last4: string | null;
|
|
81
|
+
created_at: string | null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type RemixerShippingRate = {
|
|
85
|
+
object_id: string;
|
|
86
|
+
amount: string | number;
|
|
87
|
+
currency: string;
|
|
88
|
+
provider: string;
|
|
89
|
+
provider_image_75?: string;
|
|
90
|
+
servicelevel_name: string;
|
|
91
|
+
servicelevel_token?: string;
|
|
92
|
+
estimated_days?: number | null;
|
|
93
|
+
duration_terms?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type RemixerShippingRatesParams = {
|
|
97
|
+
address_from?: Record<string, unknown>;
|
|
98
|
+
address_to: RemixerShippingAddress;
|
|
99
|
+
parcel?: Record<string, unknown>;
|
|
100
|
+
line_items?: Record<string, unknown>[];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type RemixerShippingRatesResponse = {
|
|
104
|
+
rates: RemixerShippingRate[];
|
|
105
|
+
shipment_id?: string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type RemixerAddressValidationMessage = {
|
|
109
|
+
source: string;
|
|
110
|
+
code?: string;
|
|
111
|
+
type?: string;
|
|
112
|
+
text: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type RemixerAddressValidationResponse = {
|
|
116
|
+
is_valid: boolean;
|
|
117
|
+
messages: RemixerAddressValidationMessage[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type RemixerTrackingStatus = {
|
|
121
|
+
object_created?: string;
|
|
122
|
+
object_updated?: string;
|
|
123
|
+
object_id?: string;
|
|
124
|
+
status?: string;
|
|
125
|
+
status_details?: string;
|
|
126
|
+
status_date?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type RemixerTrackingResponse = {
|
|
130
|
+
tracking_number: string;
|
|
131
|
+
carrier: string;
|
|
132
|
+
tracking_status?: RemixerTrackingStatus;
|
|
133
|
+
tracking_history?: RemixerTrackingStatus[];
|
|
134
|
+
eta?: string | null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const ecommerce = {
|
|
138
|
+
createCheckoutSession(params: RemixerCheckoutParams) {
|
|
139
|
+
return runtimeDataFetch<RemixerCheckoutSession>('remixer-runtime/ecommerce/checkout-session', {
|
|
140
|
+
line_items: params.items.map(item => ({
|
|
141
|
+
price_id: item.priceId,
|
|
142
|
+
quantity: item.quantity,
|
|
143
|
+
})),
|
|
144
|
+
mode: params.mode,
|
|
145
|
+
success_url: params.successUrl,
|
|
146
|
+
cancel_url: params.cancelUrl,
|
|
147
|
+
guest_email: params.guestEmail,
|
|
148
|
+
shipping_rate_id: params.shippingRateId,
|
|
149
|
+
shipping_cost: params.shippingCost,
|
|
150
|
+
shipping_label: params.shippingLabel,
|
|
151
|
+
shipping_address: params.shippingAddress,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
listOrders() {
|
|
156
|
+
return runtimeDataFetch<RemixerStripeOrder[]>('remixer-runtime/ecommerce/orders', {});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
getSubscription() {
|
|
160
|
+
return runtimeDataFetch<RemixerStripeSubscription | null>('remixer-runtime/ecommerce/subscription', {});
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
shipping: {
|
|
164
|
+
getRates(params: RemixerShippingRatesParams) {
|
|
165
|
+
return runtimeDataFetch<RemixerShippingRatesResponse>('remixer-runtime/shipping/rates', params as unknown as RemixerRecordData);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
validateAddress(address: RemixerShippingAddress) {
|
|
169
|
+
return runtimeDataFetch<RemixerAddressValidationResponse>('remixer-runtime/shipping/validate-address', address as unknown as RemixerRecordData);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
getTracking(carrier: string, trackingNumber: string) {
|
|
173
|
+
return runtimeDataFetch<RemixerTrackingResponse>('remixer-runtime/shipping/tracking', {
|
|
174
|
+
carrier,
|
|
175
|
+
tracking_number: trackingNumber,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|