@catandbox/schrodinger-web-adapter 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/dist/client/events.d.ts +10 -0
- package/dist/client/events.js +25 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.js +14 -0
- package/dist/client/new-ticket-form.d.ts +8 -0
- package/dist/client/new-ticket-form.js +97 -0
- package/dist/client/portal.d.ts +12 -0
- package/dist/client/portal.js +77 -0
- package/dist/client/status-badge.d.ts +2 -0
- package/dist/client/status-badge.js +11 -0
- package/dist/client/ticket-detail.d.ts +9 -0
- package/dist/client/ticket-detail.js +167 -0
- package/dist/client/ticket-list.d.ts +9 -0
- package/dist/client/ticket-list.js +81 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.js +1 -0
- package/dist/signer.d.ts +2 -0
- package/dist/signer.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple typed event emitter for state management without a framework.
|
|
3
|
+
*/
|
|
4
|
+
export type EventHandler<T = void> = (data: T) => void;
|
|
5
|
+
export declare class EventEmitter<EventMap extends Record<string, unknown> = Record<string, unknown>> {
|
|
6
|
+
private readonly _listeners;
|
|
7
|
+
on<K extends string & keyof EventMap>(event: K, handler: EventHandler<EventMap[K]>): () => void;
|
|
8
|
+
emit<K extends string & keyof EventMap>(event: K, data: EventMap[K]): void;
|
|
9
|
+
removeAllListeners(): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class EventEmitter {
|
|
2
|
+
_listeners = new Map();
|
|
3
|
+
on(event, handler) {
|
|
4
|
+
let set = this._listeners.get(event);
|
|
5
|
+
if (!set) {
|
|
6
|
+
set = new Set();
|
|
7
|
+
this._listeners.set(event, set);
|
|
8
|
+
}
|
|
9
|
+
set.add(handler);
|
|
10
|
+
return () => {
|
|
11
|
+
set.delete(handler);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
emit(event, data) {
|
|
15
|
+
const set = this._listeners.get(event);
|
|
16
|
+
if (set) {
|
|
17
|
+
for (const handler of set) {
|
|
18
|
+
handler(data);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
removeAllListeners() {
|
|
23
|
+
this._listeners.clear();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side exports — Vanilla TS + Polaris Web Components UI.
|
|
3
|
+
* No React dependency.
|
|
4
|
+
*/
|
|
5
|
+
export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
6
|
+
export type { ClientHeadersProvider, ListTicketsParams, SupportCategory, SupportClientOptions, TicketDetailData, UploadCompleteInput, UploadInitResult, UploadInputFile } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
7
|
+
export { renderSupportPortal } from "./portal";
|
|
8
|
+
export type { SupportPortalOptions } from "./portal";
|
|
9
|
+
export { renderTicketList } from "./ticket-list";
|
|
10
|
+
export type { TicketListEvents } from "./ticket-list";
|
|
11
|
+
export { renderTicketDetail } from "./ticket-detail";
|
|
12
|
+
export type { TicketDetailEvents } from "./ticket-detail";
|
|
13
|
+
export { renderNewTicketForm } from "./new-ticket-form";
|
|
14
|
+
export type { NewTicketFormEvents } from "./new-ticket-form";
|
|
15
|
+
export { renderStatusBadge } from "./status-badge";
|
|
16
|
+
export { EventEmitter } from "./events";
|
|
17
|
+
export type { EventHandler } from "./events";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side exports — Vanilla TS + Polaris Web Components UI.
|
|
3
|
+
* No React dependency.
|
|
4
|
+
*/
|
|
5
|
+
// Re-export the pure-fetch API client from the React adapter
|
|
6
|
+
export { SupportApiClient, SupportApiError } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
7
|
+
// Vanilla rendering functions
|
|
8
|
+
export { renderSupportPortal } from "./portal";
|
|
9
|
+
export { renderTicketList } from "./ticket-list";
|
|
10
|
+
export { renderTicketDetail } from "./ticket-detail";
|
|
11
|
+
export { renderNewTicketForm } from "./new-ticket-form";
|
|
12
|
+
export { renderStatusBadge } from "./status-badge";
|
|
13
|
+
// Event emitter for custom integrations
|
|
14
|
+
export { EventEmitter } from "./events";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SupportApiClient, SupportCategory } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
2
|
+
import type { EventEmitter } from "./events";
|
|
3
|
+
export interface NewTicketFormEvents {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
"ticket:created": string;
|
|
6
|
+
"ticket:cancel": void;
|
|
7
|
+
}
|
|
8
|
+
export declare function renderNewTicketForm(container: HTMLElement, client: SupportApiClient, categories: SupportCategory[], emitter: EventEmitter<NewTicketFormEvents>): void;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function renderNewTicketForm(container, client, categories, emitter) {
|
|
2
|
+
const categoryOptions = categories
|
|
3
|
+
.map((cat) => `<option value="${cat.id}">${escapeHtml(cat.name)}</option>`)
|
|
4
|
+
.join("");
|
|
5
|
+
container.innerHTML = `
|
|
6
|
+
<s-card>
|
|
7
|
+
<s-box padding="large">
|
|
8
|
+
<s-stack gap="large">
|
|
9
|
+
<s-stack gap="extraTight">
|
|
10
|
+
<s-button variant="plain" id="sch-cancel-btn">Back to tickets</s-button>
|
|
11
|
+
<s-text variant="headingLg">New Ticket</s-text>
|
|
12
|
+
</s-stack>
|
|
13
|
+
|
|
14
|
+
<form id="sch-new-ticket-form">
|
|
15
|
+
<s-stack gap="base">
|
|
16
|
+
${categories.length > 0
|
|
17
|
+
? `
|
|
18
|
+
<div>
|
|
19
|
+
<label for="sch-category" style="display:block; font-weight:600; margin-bottom:4px;">Category</label>
|
|
20
|
+
<select id="sch-category" style="width:100%; padding:8px; border:1px solid var(--p-color-border, #ccc); border-radius:8px; font-family:inherit;">
|
|
21
|
+
<option value="">Select a category...</option>
|
|
22
|
+
${categoryOptions}
|
|
23
|
+
</select>
|
|
24
|
+
</div>
|
|
25
|
+
`
|
|
26
|
+
: ""}
|
|
27
|
+
|
|
28
|
+
<div>
|
|
29
|
+
<label for="sch-title" style="display:block; font-weight:600; margin-bottom:4px;">Subject</label>
|
|
30
|
+
<input id="sch-title" type="text" required
|
|
31
|
+
style="width:100%; padding:8px; border:1px solid var(--p-color-border, #ccc); border-radius:8px; font-family:inherit; box-sizing:border-box;"
|
|
32
|
+
placeholder="Brief summary of your issue" />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div>
|
|
36
|
+
<label for="sch-body" style="display:block; font-weight:600; margin-bottom:4px;">Description</label>
|
|
37
|
+
<textarea id="sch-body" rows="6" required
|
|
38
|
+
style="width:100%; padding:8px; border:1px solid var(--p-color-border, #ccc); border-radius:8px; font-family:inherit; box-sizing:border-box;"
|
|
39
|
+
placeholder="Describe your issue in detail..."></textarea>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div id="sch-form-error"></div>
|
|
43
|
+
|
|
44
|
+
<div style="display:flex; gap:8px;">
|
|
45
|
+
<s-button variant="primary" id="sch-submit-btn" type="submit">Submit Ticket</s-button>
|
|
46
|
+
<s-button id="sch-cancel-btn-2">Cancel</s-button>
|
|
47
|
+
</div>
|
|
48
|
+
</s-stack>
|
|
49
|
+
</form>
|
|
50
|
+
</s-stack>
|
|
51
|
+
</s-box>
|
|
52
|
+
</s-card>
|
|
53
|
+
`;
|
|
54
|
+
const form = container.querySelector("#sch-new-ticket-form");
|
|
55
|
+
container.querySelector("#sch-cancel-btn")?.addEventListener("click", () => {
|
|
56
|
+
emitter.emit("ticket:cancel", undefined);
|
|
57
|
+
});
|
|
58
|
+
container.querySelector("#sch-cancel-btn-2")?.addEventListener("click", () => {
|
|
59
|
+
emitter.emit("ticket:cancel", undefined);
|
|
60
|
+
});
|
|
61
|
+
form.addEventListener("submit", async (e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const errorEl = container.querySelector("#sch-form-error");
|
|
64
|
+
errorEl.innerHTML = "";
|
|
65
|
+
const title = container.querySelector("#sch-title").value.trim();
|
|
66
|
+
const body = container.querySelector("#sch-body").value.trim();
|
|
67
|
+
const categorySelect = container.querySelector("#sch-category");
|
|
68
|
+
const categoryId = categorySelect?.value || null;
|
|
69
|
+
if (!title || !body) {
|
|
70
|
+
errorEl.innerHTML = `<s-banner tone="critical"><s-text>Subject and description are required.</s-text></s-banner>`;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const submitBtn = container.querySelector("#sch-submit-btn");
|
|
74
|
+
submitBtn.setAttribute("disabled", "true");
|
|
75
|
+
try {
|
|
76
|
+
const ticket = await client.createTicket({
|
|
77
|
+
title,
|
|
78
|
+
body,
|
|
79
|
+
categoryId: categoryId || null
|
|
80
|
+
});
|
|
81
|
+
emitter.emit("ticket:created", ticket.id);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
errorEl.innerHTML = `
|
|
85
|
+
<s-banner tone="critical">
|
|
86
|
+
<s-text>Failed to create ticket: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
|
|
87
|
+
</s-banner>
|
|
88
|
+
`;
|
|
89
|
+
submitBtn.removeAttribute("disabled");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function escapeHtml(text) {
|
|
94
|
+
const div = document.createElement("div");
|
|
95
|
+
div.textContent = text;
|
|
96
|
+
return div.innerHTML;
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SupportClientOptions, SupportCategory } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
2
|
+
export interface SupportPortalOptions extends SupportClientOptions {
|
|
3
|
+
/** Pre-loaded categories. If omitted, categories are fetched from the API. */
|
|
4
|
+
categories?: SupportCategory[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Main entry point — renders a complete support portal into the given container.
|
|
8
|
+
* Uses Polaris Web Components for UI and manages navigation between views.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderSupportPortal(container: HTMLElement, options?: SupportPortalOptions): Promise<{
|
|
11
|
+
destroy: () => void;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
2
|
+
import { EventEmitter } from "./events";
|
|
3
|
+
import { renderTicketList } from "./ticket-list";
|
|
4
|
+
import { renderTicketDetail } from "./ticket-detail";
|
|
5
|
+
import { renderNewTicketForm } from "./new-ticket-form";
|
|
6
|
+
/**
|
|
7
|
+
* Main entry point — renders a complete support portal into the given container.
|
|
8
|
+
* Uses Polaris Web Components for UI and manages navigation between views.
|
|
9
|
+
*/
|
|
10
|
+
export async function renderSupportPortal(container, options = {}) {
|
|
11
|
+
const client = new SupportApiClient(options);
|
|
12
|
+
const emitter = new EventEmitter();
|
|
13
|
+
let currentView = "list";
|
|
14
|
+
let selectedTicketId = null;
|
|
15
|
+
let categories = options.categories ?? [];
|
|
16
|
+
async function loadCategories() {
|
|
17
|
+
if (categories.length > 0)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
const result = await client.getCategories();
|
|
21
|
+
categories = result.items;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
categories = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function navigate(view, ticketId) {
|
|
28
|
+
currentView = view;
|
|
29
|
+
if (ticketId !== undefined) {
|
|
30
|
+
selectedTicketId = ticketId;
|
|
31
|
+
}
|
|
32
|
+
switch (currentView) {
|
|
33
|
+
case "list":
|
|
34
|
+
await renderTicketList(container, client, emitter);
|
|
35
|
+
break;
|
|
36
|
+
case "detail":
|
|
37
|
+
if (selectedTicketId) {
|
|
38
|
+
await renderTicketDetail(container, client, selectedTicketId, emitter);
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
case "create":
|
|
42
|
+
await loadCategories();
|
|
43
|
+
renderNewTicketForm(container, client, categories, emitter);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Wire up navigation events
|
|
48
|
+
emitter.on("ticket:select", (ticketId) => {
|
|
49
|
+
void navigate("detail", ticketId);
|
|
50
|
+
});
|
|
51
|
+
emitter.on("ticket:create", () => {
|
|
52
|
+
void navigate("create");
|
|
53
|
+
});
|
|
54
|
+
emitter.on("ticket:back", () => {
|
|
55
|
+
void navigate("list");
|
|
56
|
+
});
|
|
57
|
+
emitter.on("ticket:cancel", () => {
|
|
58
|
+
void navigate("list");
|
|
59
|
+
});
|
|
60
|
+
emitter.on("ticket:created", (ticketId) => {
|
|
61
|
+
void navigate("detail", ticketId);
|
|
62
|
+
});
|
|
63
|
+
emitter.on("ticket:closed", () => {
|
|
64
|
+
// Stay on detail view — it re-renders itself
|
|
65
|
+
});
|
|
66
|
+
emitter.on("ticket:reopened", () => {
|
|
67
|
+
// Stay on detail view — it re-renders itself
|
|
68
|
+
});
|
|
69
|
+
// Initial render
|
|
70
|
+
await navigate("list");
|
|
71
|
+
return {
|
|
72
|
+
destroy() {
|
|
73
|
+
emitter.removeAllListeners();
|
|
74
|
+
container.innerHTML = "";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const STATUS_CONFIG = {
|
|
2
|
+
Active: { label: "Active", tone: "info" },
|
|
3
|
+
InProgress: { label: "In Progress", tone: "attention" },
|
|
4
|
+
AwaitingResponse: { label: "Awaiting Response", tone: "warning" },
|
|
5
|
+
Closed: { label: "Closed", tone: "success" },
|
|
6
|
+
Archived: { label: "Archived", tone: "subdued" }
|
|
7
|
+
};
|
|
8
|
+
export function renderStatusBadge(status) {
|
|
9
|
+
const config = STATUS_CONFIG[status] ?? { label: status, tone: "subdued" };
|
|
10
|
+
return `<s-badge tone="${config.tone}">${config.label}</s-badge>`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
2
|
+
import type { EventEmitter } from "./events";
|
|
3
|
+
export interface TicketDetailEvents {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
"ticket:back": void;
|
|
6
|
+
"ticket:closed": string;
|
|
7
|
+
"ticket:reopened": string;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderTicketDetail(container: HTMLElement, client: SupportApiClient, ticketId: string, emitter: EventEmitter<TicketDetailEvents>): Promise<void>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { renderStatusBadge } from "./status-badge";
|
|
2
|
+
function formatTimestamp(timestamp) {
|
|
3
|
+
return new Date(timestamp * 1000).toLocaleString();
|
|
4
|
+
}
|
|
5
|
+
export async function renderTicketDetail(container, client, ticketId, emitter) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<s-card>
|
|
8
|
+
<s-box padding="large">
|
|
9
|
+
<s-text tone="subdued">Loading ticket...</s-text>
|
|
10
|
+
</s-box>
|
|
11
|
+
</s-card>
|
|
12
|
+
`;
|
|
13
|
+
try {
|
|
14
|
+
const data = await client.getTicket(ticketId);
|
|
15
|
+
const { ticket, messages } = data;
|
|
16
|
+
container.innerHTML = `
|
|
17
|
+
<s-card>
|
|
18
|
+
<s-box padding="large">
|
|
19
|
+
<s-stack gap="large">
|
|
20
|
+
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
|
21
|
+
<s-stack gap="extraTight">
|
|
22
|
+
<s-button variant="plain" id="sch-back-btn">Back to tickets</s-button>
|
|
23
|
+
<s-text variant="headingLg">${escapeHtml(ticket.title)}</s-text>
|
|
24
|
+
<s-text variant="bodySm" tone="subdued">#${ticket.id.slice(0, 8)} · Created ${formatTimestamp(ticket.createdAt)}</s-text>
|
|
25
|
+
</s-stack>
|
|
26
|
+
<s-stack gap="small" align="end">
|
|
27
|
+
${renderStatusBadge(ticket.status)}
|
|
28
|
+
<div id="sch-ticket-actions"></div>
|
|
29
|
+
</s-stack>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div id="sch-messages-list">
|
|
33
|
+
${messages.length === 0
|
|
34
|
+
? '<s-text tone="subdued">No messages yet.</s-text>'
|
|
35
|
+
: messages.map((msg) => renderMessage(msg)).join("")}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
${ticket.status !== "Closed" && ticket.status !== "Archived"
|
|
39
|
+
? `
|
|
40
|
+
<div id="sch-reply-section">
|
|
41
|
+
<s-stack gap="small">
|
|
42
|
+
<s-text variant="headingSm">Reply</s-text>
|
|
43
|
+
<textarea id="sch-reply-body" rows="4" style="width:100%; padding:8px; border:1px solid var(--p-color-border, #ccc); border-radius:8px; font-family:inherit; font-size:14px;" placeholder="Type your reply..."></textarea>
|
|
44
|
+
<div style="display:flex; gap:8px;">
|
|
45
|
+
<s-button variant="primary" id="sch-send-reply-btn">Send Reply</s-button>
|
|
46
|
+
<s-button id="sch-close-ticket-btn">Close Ticket</s-button>
|
|
47
|
+
</div>
|
|
48
|
+
</s-stack>
|
|
49
|
+
</div>
|
|
50
|
+
`
|
|
51
|
+
: `
|
|
52
|
+
<div>
|
|
53
|
+
<s-button id="sch-reopen-ticket-btn">Reopen Ticket</s-button>
|
|
54
|
+
</div>
|
|
55
|
+
`}
|
|
56
|
+
</s-stack>
|
|
57
|
+
</s-box>
|
|
58
|
+
</s-card>
|
|
59
|
+
`;
|
|
60
|
+
// Back button
|
|
61
|
+
container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
|
|
62
|
+
emitter.emit("ticket:back", undefined);
|
|
63
|
+
});
|
|
64
|
+
// Send reply
|
|
65
|
+
container.querySelector("#sch-send-reply-btn")?.addEventListener("click", async () => {
|
|
66
|
+
const textarea = container.querySelector("#sch-reply-body");
|
|
67
|
+
const body = textarea?.value.trim();
|
|
68
|
+
if (!body)
|
|
69
|
+
return;
|
|
70
|
+
const btn = container.querySelector("#sch-send-reply-btn");
|
|
71
|
+
if (btn)
|
|
72
|
+
btn.setAttribute("disabled", "true");
|
|
73
|
+
try {
|
|
74
|
+
await client.createReply(ticketId, { body });
|
|
75
|
+
// Re-render to show the new message
|
|
76
|
+
await renderTicketDetail(container, client, ticketId, emitter);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
showError(container, error instanceof Error ? error.message : "Failed to send reply");
|
|
80
|
+
if (btn)
|
|
81
|
+
btn.removeAttribute("disabled");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Close ticket
|
|
85
|
+
container.querySelector("#sch-close-ticket-btn")?.addEventListener("click", async () => {
|
|
86
|
+
try {
|
|
87
|
+
await client.closeTicket(ticketId);
|
|
88
|
+
emitter.emit("ticket:closed", ticketId);
|
|
89
|
+
await renderTicketDetail(container, client, ticketId, emitter);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
showError(container, error instanceof Error ? error.message : "Failed to close ticket");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Reopen ticket
|
|
96
|
+
container.querySelector("#sch-reopen-ticket-btn")?.addEventListener("click", async () => {
|
|
97
|
+
try {
|
|
98
|
+
await client.reopenTicket(ticketId);
|
|
99
|
+
emitter.emit("ticket:reopened", ticketId);
|
|
100
|
+
await renderTicketDetail(container, client, ticketId, emitter);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
showError(container, error instanceof Error ? error.message : "Failed to reopen ticket");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
container.innerHTML = `
|
|
109
|
+
<s-card>
|
|
110
|
+
<s-box padding="large">
|
|
111
|
+
<s-banner tone="critical">
|
|
112
|
+
<s-text>Failed to load ticket: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
|
|
113
|
+
</s-banner>
|
|
114
|
+
<s-button variant="plain" id="sch-back-btn" style="margin-top:12px;">Back to tickets</s-button>
|
|
115
|
+
</s-box>
|
|
116
|
+
</s-card>
|
|
117
|
+
`;
|
|
118
|
+
container.querySelector("#sch-back-btn")?.addEventListener("click", () => {
|
|
119
|
+
emitter.emit("ticket:back", undefined);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function renderMessage(msg) {
|
|
124
|
+
const isSystem = msg.authorType === "system";
|
|
125
|
+
const isAgent = msg.authorType === "agent" || msg.authorType === "admin";
|
|
126
|
+
const align = isSystem ? "center" : isAgent ? "flex-start" : "flex-end";
|
|
127
|
+
const bg = isSystem ? "surface-secondary" : isAgent ? "surface-secondary" : "surface-selected";
|
|
128
|
+
const label = isSystem ? "System" : isAgent ? "Support" : "You";
|
|
129
|
+
return `
|
|
130
|
+
<s-box padding="base" border-radius="base" background="${bg}" style="align-self:${align}; margin-bottom:8px;">
|
|
131
|
+
<s-stack gap="extraTight">
|
|
132
|
+
<div style="display:flex; justify-content:space-between;">
|
|
133
|
+
<s-text variant="bodySm" fontWeight="semibold">${label}</s-text>
|
|
134
|
+
<s-text variant="bodySm" tone="subdued">${formatTimestamp(msg.createdAt)}</s-text>
|
|
135
|
+
</div>
|
|
136
|
+
<s-text variant="bodyMd">${escapeHtml(msg.bodyPlain)}</s-text>
|
|
137
|
+
</s-stack>
|
|
138
|
+
</s-box>
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
function showError(container, message) {
|
|
142
|
+
const existing = container.querySelector("#sch-error-banner");
|
|
143
|
+
if (existing)
|
|
144
|
+
existing.remove();
|
|
145
|
+
const banner = document.createElement("div");
|
|
146
|
+
banner.id = "sch-error-banner";
|
|
147
|
+
banner.innerHTML = `
|
|
148
|
+
<s-banner tone="critical" style="margin-top:8px;">
|
|
149
|
+
<s-text>${escapeHtml(message)}</s-text>
|
|
150
|
+
</s-banner>
|
|
151
|
+
`;
|
|
152
|
+
const replySection = container.querySelector("#sch-reply-section");
|
|
153
|
+
if (replySection) {
|
|
154
|
+
replySection.insertBefore(banner, replySection.firstChild);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const card = container.querySelector("s-card");
|
|
158
|
+
if (card) {
|
|
159
|
+
card.insertAdjacentElement("beforeend", banner);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function escapeHtml(text) {
|
|
164
|
+
const div = document.createElement("div");
|
|
165
|
+
div.textContent = text;
|
|
166
|
+
return div.innerHTML;
|
|
167
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TicketStatus } from "@catandbox/schrodinger-contracts";
|
|
2
|
+
import type { SupportApiClient } from "@catandbox/schrodinger-shopify-adapter/client";
|
|
3
|
+
import type { EventEmitter } from "./events";
|
|
4
|
+
export interface TicketListEvents {
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
"ticket:select": string;
|
|
7
|
+
"ticket:create": void;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderTicketList(container: HTMLElement, client: SupportApiClient, emitter: EventEmitter<TicketListEvents>, statusFilter?: TicketStatus): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { renderStatusBadge } from "./status-badge";
|
|
2
|
+
function formatDate(timestamp) {
|
|
3
|
+
return new Date(timestamp * 1000).toLocaleDateString(undefined, {
|
|
4
|
+
month: "short",
|
|
5
|
+
day: "numeric",
|
|
6
|
+
year: "numeric"
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function renderTicketList(container, client, emitter, statusFilter) {
|
|
10
|
+
container.innerHTML = `
|
|
11
|
+
<s-card>
|
|
12
|
+
<s-box padding="large">
|
|
13
|
+
<s-stack gap="base">
|
|
14
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
15
|
+
<s-text variant="headingMd">Support Tickets</s-text>
|
|
16
|
+
<s-button variant="primary" id="sch-new-ticket-btn">New Ticket</s-button>
|
|
17
|
+
</div>
|
|
18
|
+
<div id="sch-ticket-list-body">
|
|
19
|
+
<s-text tone="subdued">Loading tickets...</s-text>
|
|
20
|
+
</div>
|
|
21
|
+
</s-stack>
|
|
22
|
+
</s-box>
|
|
23
|
+
</s-card>
|
|
24
|
+
`;
|
|
25
|
+
const body = container.querySelector("#sch-ticket-list-body");
|
|
26
|
+
container
|
|
27
|
+
.querySelector("#sch-new-ticket-btn")
|
|
28
|
+
?.addEventListener("click", () => emitter.emit("ticket:create", undefined));
|
|
29
|
+
try {
|
|
30
|
+
const result = await client.listTickets(statusFilter ? { status: statusFilter } : {});
|
|
31
|
+
const tickets = result.items;
|
|
32
|
+
if (tickets.length === 0) {
|
|
33
|
+
body.innerHTML = `
|
|
34
|
+
<s-box padding="large" border-radius="base" background="surface-secondary">
|
|
35
|
+
<s-stack gap="small" align="center">
|
|
36
|
+
<s-text tone="subdued">No tickets yet.</s-text>
|
|
37
|
+
<s-text variant="bodySm" tone="subdued">Create a new ticket to get started.</s-text>
|
|
38
|
+
</s-stack>
|
|
39
|
+
</s-box>
|
|
40
|
+
`;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
body.innerHTML = `
|
|
44
|
+
<s-resource-list>
|
|
45
|
+
${tickets
|
|
46
|
+
.map((ticket) => `
|
|
47
|
+
<s-resource-item id="sch-ticket-${ticket.id}" data-ticket-id="${ticket.id}">
|
|
48
|
+
<div style="display:flex; justify-content:space-between; align-items:center; width:100%; cursor:pointer;">
|
|
49
|
+
<s-stack gap="extraTight">
|
|
50
|
+
<s-text variant="bodyMd" fontWeight="semibold">${escapeHtml(ticket.title)}</s-text>
|
|
51
|
+
<s-text variant="bodySm" tone="subdued">#${ticket.id.slice(0, 8)} · ${formatDate(ticket.createdAt)}</s-text>
|
|
52
|
+
</s-stack>
|
|
53
|
+
${renderStatusBadge(ticket.status)}
|
|
54
|
+
</div>
|
|
55
|
+
</s-resource-item>
|
|
56
|
+
`)
|
|
57
|
+
.join("")}
|
|
58
|
+
</s-resource-list>
|
|
59
|
+
`;
|
|
60
|
+
container.querySelectorAll("[data-ticket-id]").forEach((el) => {
|
|
61
|
+
el.addEventListener("click", () => {
|
|
62
|
+
const ticketId = el.getAttribute("data-ticket-id");
|
|
63
|
+
if (ticketId) {
|
|
64
|
+
emitter.emit("ticket:select", ticketId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
body.innerHTML = `
|
|
71
|
+
<s-banner tone="critical">
|
|
72
|
+
<s-text>Failed to load tickets: ${escapeHtml(error instanceof Error ? error.message : String(error))}</s-text>
|
|
73
|
+
</s-banner>
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function escapeHtml(text) {
|
|
78
|
+
const div = document.createElement("div");
|
|
79
|
+
div.textContent = text;
|
|
80
|
+
return div.innerHTML;
|
|
81
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client/index";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client/index";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side exports — re-exported from the React adapter's server module.
|
|
3
|
+
* These are already framework-agnostic (no React dependency).
|
|
4
|
+
*/
|
|
5
|
+
export type { AdapterEnvironment, PrincipalContext, ProxyHandlerOptions, ShopifySessionVerificationOptions, WebhookForwardingOptions } from "@catandbox/schrodinger-shopify-adapter/server";
|
|
6
|
+
export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac, signSchrodingerRequest, createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "@catandbox/schrodinger-shopify-adapter/server";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ShopifyAuthError, createPrincipalContext, extractShopifySessionToken, verifyShopifySessionToken, verifyShopifyWebhookHmac, signSchrodingerRequest, createShopifyProxyHandler, createShopifyWebhookHandlers, handleAppUninstalledWebhook, handleCustomersDataRequestWebhook, handleCustomersRedactWebhook, handleShopRedactWebhook, parsePrefillRoute } from "@catandbox/schrodinger-shopify-adapter/server";
|
package/dist/signer.d.ts
ADDED
package/dist/signer.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { signRequest, sha256Hex, buildCanonicalString } from "@catandbox/schrodinger-shopify-adapter/signer";
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@catandbox/schrodinger-web-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"types": "./dist/server/index.d.ts",
|
|
15
|
+
"import": "./dist/server/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./dist/client/index.d.ts",
|
|
19
|
+
"import": "./dist/client/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./signer": {
|
|
22
|
+
"types": "./dist/signer.d.ts",
|
|
23
|
+
"import": "./dist/signer.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.build.json",
|
|
32
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
33
|
+
"lint": "eslint src test --ext .ts",
|
|
34
|
+
"test": "vitest run"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@catandbox/schrodinger-contracts": "^0.1.0",
|
|
38
|
+
"@catandbox/schrodinger-shopify-adapter": "^0.1.0"
|
|
39
|
+
}
|
|
40
|
+
}
|