@jonsoc/console-app 1.1.34

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.
Files changed (217) hide show
  1. package/.opencode/agent/css.md +149 -0
  2. package/README.md +32 -0
  3. package/package.json +49 -0
  4. package/public/apple-touch-icon-v3.png +1 -0
  5. package/public/apple-touch-icon.png +1 -0
  6. package/public/email +1 -0
  7. package/public/favicon-96x96-v3.png +1 -0
  8. package/public/favicon-96x96.png +1 -0
  9. package/public/favicon-v3.ico +1 -0
  10. package/public/favicon-v3.svg +1 -0
  11. package/public/favicon.ico +1 -0
  12. package/public/favicon.svg +1 -0
  13. package/public/opencode-brand-assets.zip +0 -0
  14. package/public/robots.txt +6 -0
  15. package/public/site.webmanifest +1 -0
  16. package/public/social-share-black.png +1 -0
  17. package/public/social-share-zen.png +1 -0
  18. package/public/social-share.png +1 -0
  19. package/public/theme.json +182 -0
  20. package/public/web-app-manifest-192x192.png +1 -0
  21. package/public/web-app-manifest-512x512.png +1 -0
  22. package/script/generate-sitemap.ts +103 -0
  23. package/src/app.css +1 -0
  24. package/src/app.tsx +27 -0
  25. package/src/asset/black/hero.png +0 -0
  26. package/src/asset/brand/opencode-brand-assets.zip +0 -0
  27. package/src/asset/brand/opencode-logo-dark.png +0 -0
  28. package/src/asset/brand/opencode-logo-dark.svg +16 -0
  29. package/src/asset/brand/opencode-logo-light.png +0 -0
  30. package/src/asset/brand/opencode-logo-light.svg +16 -0
  31. package/src/asset/brand/opencode-wordmark-dark.png +0 -0
  32. package/src/asset/brand/opencode-wordmark-dark.svg +30 -0
  33. package/src/asset/brand/opencode-wordmark-light.png +0 -0
  34. package/src/asset/brand/opencode-wordmark-light.svg +30 -0
  35. package/src/asset/brand/opencode-wordmark-simple-dark.png +0 -0
  36. package/src/asset/brand/opencode-wordmark-simple-dark.svg +22 -0
  37. package/src/asset/brand/opencode-wordmark-simple-light.png +0 -0
  38. package/src/asset/brand/opencode-wordmark-simple-light.svg +22 -0
  39. package/src/asset/brand/preview-opencode-dark.png +0 -0
  40. package/src/asset/brand/preview-opencode-logo-dark.png +0 -0
  41. package/src/asset/brand/preview-opencode-logo-light.png +0 -0
  42. package/src/asset/brand/preview-opencode-wordmark-dark.png +0 -0
  43. package/src/asset/brand/preview-opencode-wordmark-light.png +0 -0
  44. package/src/asset/brand/preview-opencode-wordmark-simple-dark.png +0 -0
  45. package/src/asset/brand/preview-opencode-wordmark-simple-light.png +0 -0
  46. package/src/asset/lander/avatar-adam.png +0 -0
  47. package/src/asset/lander/avatar-david.png +0 -0
  48. package/src/asset/lander/avatar-dax.png +0 -0
  49. package/src/asset/lander/avatar-frank.png +0 -0
  50. package/src/asset/lander/avatar-jay.png +0 -0
  51. package/src/asset/lander/brand-assets-dark.svg +10 -0
  52. package/src/asset/lander/brand-assets-light.svg +10 -0
  53. package/src/asset/lander/brand.png +0 -0
  54. package/src/asset/lander/check.svg +3 -0
  55. package/src/asset/lander/copy.svg +3 -0
  56. package/src/asset/lander/desktop-app-icon.png +0 -0
  57. package/src/asset/lander/dock.png +0 -0
  58. package/src/asset/lander/logo-dark.svg +11 -0
  59. package/src/asset/lander/logo-light.svg +11 -0
  60. package/src/asset/lander/opencode-comparison-min.mp4 +0 -0
  61. package/src/asset/lander/opencode-comparison-poster.png +0 -0
  62. package/src/asset/lander/opencode-desktop-icon.png +0 -0
  63. package/src/asset/lander/opencode-logo-dark.svg +11 -0
  64. package/src/asset/lander/opencode-logo-light.svg +11 -0
  65. package/src/asset/lander/opencode-min.mp4 +0 -0
  66. package/src/asset/lander/opencode-poster.png +0 -0
  67. package/src/asset/lander/opencode-wordmark-dark.svg +25 -0
  68. package/src/asset/lander/opencode-wordmark-light.svg +25 -0
  69. package/src/asset/lander/screenshot-github.png +0 -0
  70. package/src/asset/lander/screenshot-splash.png +0 -0
  71. package/src/asset/lander/screenshot-vscode.png +0 -0
  72. package/src/asset/lander/screenshot.png +0 -0
  73. package/src/asset/lander/wordmark-dark.svg +3 -0
  74. package/src/asset/lander/wordmark-light.svg +3 -0
  75. package/src/asset/logo-ornate-dark.svg +18 -0
  76. package/src/asset/logo-ornate-light.svg +18 -0
  77. package/src/asset/logo.svg +18 -0
  78. package/src/asset/zen-ornate-dark.svg +8 -0
  79. package/src/asset/zen-ornate-light.svg +8 -0
  80. package/src/component/dropdown.css +80 -0
  81. package/src/component/dropdown.tsx +79 -0
  82. package/src/component/email-signup.tsx +48 -0
  83. package/src/component/faq.tsx +33 -0
  84. package/src/component/footer.tsx +38 -0
  85. package/src/component/header-context-menu.css +63 -0
  86. package/src/component/header.tsx +279 -0
  87. package/src/component/icon.tsx +257 -0
  88. package/src/component/legal.tsx +20 -0
  89. package/src/component/modal.css +66 -0
  90. package/src/component/modal.tsx +24 -0
  91. package/src/component/spotlight.css +15 -0
  92. package/src/component/spotlight.tsx +820 -0
  93. package/src/config.ts +29 -0
  94. package/src/context/auth.session.ts +0 -0
  95. package/src/context/auth.ts +116 -0
  96. package/src/context/auth.withActor.ts +7 -0
  97. package/src/entry-client.tsx +4 -0
  98. package/src/entry-server.tsx +30 -0
  99. package/src/global.d.ts +5 -0
  100. package/src/lib/github.ts +38 -0
  101. package/src/middleware.ts +5 -0
  102. package/src/routes/[...404].css +130 -0
  103. package/src/routes/[...404].tsx +38 -0
  104. package/src/routes/api/enterprise.ts +47 -0
  105. package/src/routes/auth/[...callback].ts +41 -0
  106. package/src/routes/auth/authorize.ts +10 -0
  107. package/src/routes/auth/index.ts +12 -0
  108. package/src/routes/auth/logout.ts +17 -0
  109. package/src/routes/auth/status.ts +7 -0
  110. package/src/routes/bench/[id].tsx +365 -0
  111. package/src/routes/bench/index.tsx +86 -0
  112. package/src/routes/bench/submission.ts +29 -0
  113. package/src/routes/black/common.tsx +62 -0
  114. package/src/routes/black/index.tsx +108 -0
  115. package/src/routes/black/subscribe/[plan].tsx +449 -0
  116. package/src/routes/black/workspace.css +214 -0
  117. package/src/routes/black/workspace.tsx +229 -0
  118. package/src/routes/black.css +828 -0
  119. package/src/routes/black.tsx +285 -0
  120. package/src/routes/brand/index.css +555 -0
  121. package/src/routes/brand/index.tsx +252 -0
  122. package/src/routes/changelog/index.css +477 -0
  123. package/src/routes/changelog/index.tsx +147 -0
  124. package/src/routes/debug/index.ts +13 -0
  125. package/src/routes/desktop-feedback.ts +5 -0
  126. package/src/routes/discord.ts +5 -0
  127. package/src/routes/docs/[...path].ts +20 -0
  128. package/src/routes/docs/index.ts +20 -0
  129. package/src/routes/download/[platform].ts +38 -0
  130. package/src/routes/download/index.css +750 -0
  131. package/src/routes/download/index.tsx +482 -0
  132. package/src/routes/download/types.ts +4 -0
  133. package/src/routes/enterprise/index.css +578 -0
  134. package/src/routes/enterprise/index.tsx +251 -0
  135. package/src/routes/index.css +1251 -0
  136. package/src/routes/index.tsx +840 -0
  137. package/src/routes/legal/privacy-policy/index.css +343 -0
  138. package/src/routes/legal/privacy-policy/index.tsx +1512 -0
  139. package/src/routes/legal/terms-of-service/index.css +254 -0
  140. package/src/routes/legal/terms-of-service/index.tsx +512 -0
  141. package/src/routes/openapi.json.ts +7 -0
  142. package/src/routes/s/[id].ts +20 -0
  143. package/src/routes/stripe/webhook.ts +532 -0
  144. package/src/routes/t/[...path].tsx +20 -0
  145. package/src/routes/temp.tsx +172 -0
  146. package/src/routes/user-menu.css +18 -0
  147. package/src/routes/user-menu.tsx +32 -0
  148. package/src/routes/workspace/[id]/billing/billing-section.module.css +185 -0
  149. package/src/routes/workspace/[id]/billing/billing-section.tsx +240 -0
  150. package/src/routes/workspace/[id]/billing/black-section.module.css +142 -0
  151. package/src/routes/workspace/[id]/billing/black-section.tsx +269 -0
  152. package/src/routes/workspace/[id]/billing/black-waitlist-section.module.css +23 -0
  153. package/src/routes/workspace/[id]/billing/index.tsx +32 -0
  154. package/src/routes/workspace/[id]/billing/monthly-limit-section.module.css +96 -0
  155. package/src/routes/workspace/[id]/billing/monthly-limit-section.tsx +133 -0
  156. package/src/routes/workspace/[id]/billing/payment-section.module.css +93 -0
  157. package/src/routes/workspace/[id]/billing/payment-section.tsx +122 -0
  158. package/src/routes/workspace/[id]/billing/reload-section.module.css +261 -0
  159. package/src/routes/workspace/[id]/billing/reload-section.tsx +213 -0
  160. package/src/routes/workspace/[id]/graph-section.module.css +145 -0
  161. package/src/routes/workspace/[id]/graph-section.tsx +475 -0
  162. package/src/routes/workspace/[id]/index.tsx +81 -0
  163. package/src/routes/workspace/[id]/keys/index.tsx +11 -0
  164. package/src/routes/workspace/[id]/keys/key-section.module.css +197 -0
  165. package/src/routes/workspace/[id]/keys/key-section.tsx +176 -0
  166. package/src/routes/workspace/[id]/members/index.tsx +11 -0
  167. package/src/routes/workspace/[id]/members/member-section.module.css +249 -0
  168. package/src/routes/workspace/[id]/members/member-section.tsx +343 -0
  169. package/src/routes/workspace/[id]/members/role-dropdown.css +72 -0
  170. package/src/routes/workspace/[id]/members/role-dropdown.tsx +43 -0
  171. package/src/routes/workspace/[id]/model-section.module.css +173 -0
  172. package/src/routes/workspace/[id]/model-section.tsx +174 -0
  173. package/src/routes/workspace/[id]/new-user-section.module.css +143 -0
  174. package/src/routes/workspace/[id]/new-user-section.tsx +104 -0
  175. package/src/routes/workspace/[id]/provider-section.module.css +138 -0
  176. package/src/routes/workspace/[id]/provider-section.tsx +188 -0
  177. package/src/routes/workspace/[id]/settings/index.tsx +11 -0
  178. package/src/routes/workspace/[id]/settings/settings-section.module.css +94 -0
  179. package/src/routes/workspace/[id]/settings/settings-section.tsx +122 -0
  180. package/src/routes/workspace/[id]/usage-section.module.css +185 -0
  181. package/src/routes/workspace/[id]/usage-section.tsx +200 -0
  182. package/src/routes/workspace/[id].css +308 -0
  183. package/src/routes/workspace/[id].tsx +62 -0
  184. package/src/routes/workspace/common.tsx +120 -0
  185. package/src/routes/workspace-picker.css +74 -0
  186. package/src/routes/workspace-picker.tsx +122 -0
  187. package/src/routes/workspace.css +107 -0
  188. package/src/routes/workspace.tsx +38 -0
  189. package/src/routes/zen/index.css +866 -0
  190. package/src/routes/zen/index.tsx +343 -0
  191. package/src/routes/zen/util/dataDumper.ts +44 -0
  192. package/src/routes/zen/util/error.ts +13 -0
  193. package/src/routes/zen/util/handler.ts +784 -0
  194. package/src/routes/zen/util/logger.ts +12 -0
  195. package/src/routes/zen/util/provider/anthropic.ts +752 -0
  196. package/src/routes/zen/util/provider/google.ts +75 -0
  197. package/src/routes/zen/util/provider/openai-compatible.ts +546 -0
  198. package/src/routes/zen/util/provider/openai.ts +630 -0
  199. package/src/routes/zen/util/provider/provider.ts +210 -0
  200. package/src/routes/zen/util/rateLimiter.ts +41 -0
  201. package/src/routes/zen/util/stickyProviderTracker.ts +16 -0
  202. package/src/routes/zen/util/trialLimiter.ts +49 -0
  203. package/src/routes/zen/v1/chat/completions.ts +11 -0
  204. package/src/routes/zen/v1/messages.ts +11 -0
  205. package/src/routes/zen/v1/models/[model].ts +13 -0
  206. package/src/routes/zen/v1/models.ts +60 -0
  207. package/src/routes/zen/v1/responses.ts +11 -0
  208. package/src/style/base.css +21 -0
  209. package/src/style/component/button.css +102 -0
  210. package/src/style/index.css +8 -0
  211. package/src/style/reset.css +76 -0
  212. package/src/style/token/color.css +91 -0
  213. package/src/style/token/font.css +21 -0
  214. package/src/style/token/space.css +46 -0
  215. package/sst-env.d.ts +9 -0
  216. package/tsconfig.json +21 -0
  217. package/vite.config.ts +25 -0
@@ -0,0 +1,93 @@
1
+ .root {
2
+ [data-slot="payments-table"] {
3
+ overflow-x: auto;
4
+ }
5
+
6
+ [data-slot="payments-table-element"] {
7
+ width: 100%;
8
+ border-collapse: collapse;
9
+ font-size: var(--font-size-sm);
10
+
11
+ thead {
12
+ border-bottom: 1px solid var(--color-border);
13
+ }
14
+
15
+ th {
16
+ padding: var(--space-3) var(--space-4);
17
+ text-align: left;
18
+ font-weight: normal;
19
+ color: var(--color-text-muted);
20
+ text-transform: uppercase;
21
+ }
22
+
23
+ td {
24
+ padding: var(--space-3) var(--space-4);
25
+ border-bottom: 1px solid var(--color-border-muted);
26
+ color: var(--color-text-muted);
27
+ font-family: var(--font-mono);
28
+
29
+ &[data-slot="payment-date"] {
30
+ color: var(--color-text);
31
+ }
32
+
33
+ &[data-slot="payment-id"] {
34
+ font-family: var(--font-mono);
35
+ font-weight: 400;
36
+ color: var(--color-text-muted);
37
+ max-width: 200px;
38
+ word-break: break-word;
39
+ }
40
+
41
+ &[data-slot="payment-amount"] {
42
+ color: var(--color-text);
43
+
44
+ &[data-refunded="true"] {
45
+ text-decoration: line-through;
46
+ }
47
+ }
48
+
49
+ &[data-slot="payment-receipt"] {
50
+ span {
51
+ display: inline-block;
52
+ padding: var(--space-3) var(--space-4);
53
+ font-size: var(--font-size-sm);
54
+ line-height: 1.5;
55
+ }
56
+
57
+ button {
58
+ font-size: var(--font-size-sm);
59
+ }
60
+ }
61
+ }
62
+
63
+ tbody tr {
64
+ &:last-child td {
65
+ border-bottom: none;
66
+ }
67
+ }
68
+
69
+ @media (max-width: 40rem) {
70
+ th,
71
+ td {
72
+ padding: var(--space-2) var(--space-3);
73
+ font-size: var(--font-size-xs);
74
+ }
75
+
76
+ th {
77
+ &:nth-child(2)
78
+
79
+ /* Payment ID */ {
80
+ display: none;
81
+ }
82
+ }
83
+
84
+ td {
85
+ &:nth-child(2)
86
+
87
+ /* Payment ID */ {
88
+ display: none;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,122 @@
1
+ import { Billing } from "@jonsoc/console-core/billing.js"
2
+ import { query, action, useParams, createAsync, useAction } from "@solidjs/router"
3
+ import { For, Match, Show, Switch } from "solid-js"
4
+ import { withActor } from "~/context/auth.withActor"
5
+ import { formatDateUTC, formatDateForTable } from "../../common"
6
+ import styles from "./payment-section.module.css"
7
+
8
+ const getPaymentsInfo = query(async (workspaceID: string) => {
9
+ "use server"
10
+ return withActor(async () => {
11
+ return await Billing.payments()
12
+ }, workspaceID)
13
+ }, "payment.list")
14
+
15
+ const downloadReceipt = action(async (workspaceID: string, paymentID: string) => {
16
+ "use server"
17
+ return withActor(() => Billing.generateReceiptUrl({ paymentID }), workspaceID)
18
+ }, "receipt.download")
19
+
20
+ export function PaymentSection() {
21
+ const params = useParams()
22
+ const payments = createAsync(() => getPaymentsInfo(params.id!))
23
+ const downloadReceiptAction = useAction(downloadReceipt)
24
+
25
+ // DUMMY DATA FOR TESTING
26
+ // const payments = () => [
27
+ // {
28
+ // id: "pi_3QK1x2FT9vXn4A6r1234567890",
29
+ // paymentID: "pi_3QK1x2FT9vXn4A6r1234567890",
30
+ // timeCreated: new Date(Date.now() - 86400000 * 1).toISOString(), // 1 day ago
31
+ // amount: 2100000000, // $21.00 ($20 + $1 fee)
32
+ // },
33
+ // {
34
+ // id: "pi_3QJ8k7FT9vXn4A6r0987654321",
35
+ // paymentID: "pi_3QJ8k7FT9vXn4A6r0987654321",
36
+ // timeCreated: new Date(Date.now() - 86400000 * 15).toISOString(), // 15 days ago
37
+ // amount: 2100000000, // $21.00
38
+ // },
39
+ // {
40
+ // id: "pi_3QI5m1FT9vXn4A6r5678901234",
41
+ // paymentID: "pi_3QI5m1FT9vXn4A6r5678901234",
42
+ // timeCreated: new Date(Date.now() - 86400000 * 32).toISOString(), // 32 days ago
43
+ // amount: 2100000000, // $21.00
44
+ // },
45
+ // {
46
+ // id: "pi_3QH2n9FT9vXn4A6r3456789012",
47
+ // paymentID: "pi_3QH2n9FT9vXn4A6r3456789012",
48
+ // timeCreated: new Date(Date.now() - 86400000 * 47).toISOString(), // 47 days ago
49
+ // amount: 2100000000, // $21.00
50
+ // },
51
+ // {
52
+ // id: "pi_3QG7p4FT9vXn4A6r7890123456",
53
+ // paymentID: "pi_3QG7p4FT9vXn4A6r7890123456",
54
+ // timeCreated: new Date(Date.now() - 86400000 * 63).toISOString(), // 63 days ago
55
+ // amount: 2100000000, // $21.00
56
+ // },
57
+ // ]
58
+
59
+ return (
60
+ <Show when={payments() && payments()!.length > 0}>
61
+ <section class={styles.root}>
62
+ <div data-slot="section-title">
63
+ <h2>Payments History</h2>
64
+ <p>Recent payment transactions.</p>
65
+ </div>
66
+ <div data-slot="payments-table">
67
+ <table data-slot="payments-table-element">
68
+ <thead>
69
+ <tr>
70
+ <th>Date</th>
71
+ <th>Payment ID</th>
72
+ <th>Amount</th>
73
+ <th>Receipt</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ <For each={payments()!}>
78
+ {(payment) => {
79
+ const date = new Date(payment.timeCreated)
80
+ const amount =
81
+ payment.enrichment?.type === "subscription" && payment.enrichment.couponID ? 0 : payment.amount
82
+ return (
83
+ <tr>
84
+ <td data-slot="payment-date" title={formatDateUTC(date)}>
85
+ {formatDateForTable(date)}
86
+ </td>
87
+ <td data-slot="payment-id">{payment.id}</td>
88
+ <td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
89
+ ${((amount ?? 0) / 100000000).toFixed(2)}
90
+ <Switch>
91
+ <Match when={payment.enrichment?.type === "credit"}> (credit)</Match>
92
+ <Match when={payment.enrichment?.type === "subscription"}> (subscription)</Match>
93
+ </Switch>
94
+ </td>
95
+ <td data-slot="payment-receipt">
96
+ {payment.paymentID ? (
97
+ <button
98
+ onClick={async () => {
99
+ const receiptUrl = await downloadReceiptAction(params.id!, payment.paymentID!)
100
+ if (receiptUrl) {
101
+ window.open(receiptUrl, "_blank")
102
+ }
103
+ }}
104
+ data-slot="receipt-button"
105
+ >
106
+ View
107
+ </button>
108
+ ) : (
109
+ <span>-</span>
110
+ )}
111
+ </td>
112
+ </tr>
113
+ )
114
+ }}
115
+ </For>
116
+ </tbody>
117
+ </table>
118
+ </div>
119
+ </section>
120
+ </Show>
121
+ )
122
+ }
@@ -0,0 +1,261 @@
1
+ .root {
2
+ [data-slot="title-row"] {
3
+ display: flex;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ gap: var(--space-4);
7
+ }
8
+
9
+ [data-slot="section-content"] {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--space-3);
13
+ }
14
+
15
+ [data-slot="setting-row"] {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: var(--space-3);
19
+
20
+ p {
21
+ flex: 1;
22
+ font-size: var(--font-size-sm);
23
+ line-height: 1.5;
24
+ color: var(--color-text-secondary);
25
+ margin: 0;
26
+
27
+ b {
28
+ font-weight: 600;
29
+ }
30
+ }
31
+
32
+ [data-slot="create-form"] {
33
+ margin: 0;
34
+ }
35
+ }
36
+
37
+ [data-slot="create-form"] {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: var(--space-3);
41
+ padding: var(--space-4);
42
+ border: 1px solid var(--color-border);
43
+ border-radius: var(--border-radius-sm);
44
+ margin-top: var(--space-4);
45
+
46
+ [data-slot="form-field"] {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: var(--space-2);
50
+
51
+ label {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: var(--space-2);
55
+ }
56
+
57
+ [data-slot="field-label"] {
58
+ font-size: var(--font-size-sm);
59
+ font-weight: 500;
60
+ color: var(--color-text-muted);
61
+ }
62
+
63
+ [data-slot="toggle-container"] {
64
+ display: flex;
65
+ align-items: center;
66
+ }
67
+
68
+ input[data-component="input"] {
69
+ flex: 1;
70
+ padding: var(--space-2) var(--space-3);
71
+ border: 1px solid var(--color-border);
72
+ border-radius: var(--border-radius-sm);
73
+ background-color: var(--color-bg);
74
+ color: var(--color-text);
75
+ font-size: var(--font-size-sm);
76
+ font-family: var(--font-mono);
77
+
78
+ &:focus {
79
+ outline: none;
80
+ border-color: var(--color-accent);
81
+ }
82
+
83
+ &::placeholder {
84
+ color: var(--color-text-disabled);
85
+ }
86
+ }
87
+ }
88
+
89
+ [data-slot="input-row"] {
90
+ display: flex;
91
+ flex-direction: row;
92
+ gap: var(--space-3);
93
+
94
+ @media (max-width: 40rem) {
95
+ flex-direction: column;
96
+ gap: var(--space-2);
97
+ }
98
+ }
99
+
100
+ [data-slot="input-field"] {
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: var(--space-1);
104
+ flex: 1;
105
+
106
+ p {
107
+ line-height: 1.2;
108
+ margin: 0;
109
+ color: var(--color-text-muted);
110
+ font-size: var(--font-size-sm);
111
+ }
112
+
113
+ input[data-component="input"] {
114
+ flex: 1;
115
+ padding: var(--space-2) var(--space-3);
116
+ border: 1px solid var(--color-border);
117
+ border-radius: var(--border-radius-sm);
118
+ background-color: var(--color-bg);
119
+ color: var(--color-text);
120
+ font-size: var(--font-size-sm);
121
+ line-height: 1.5;
122
+ min-width: 0;
123
+
124
+ &:focus {
125
+ outline: none;
126
+ border-color: var(--color-accent);
127
+ box-shadow: 0 0 0 3px var(--color-accent-alpha);
128
+ }
129
+
130
+ &::placeholder {
131
+ color: var(--color-text-disabled);
132
+ }
133
+
134
+ &:disabled {
135
+ opacity: 0.5;
136
+ cursor: not-allowed;
137
+ background-color: var(--color-bg-surface);
138
+ }
139
+ }
140
+
141
+ [data-slot="field-with-connector"] {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: var(--space-2);
145
+
146
+ [data-slot="field-connector"] {
147
+ font-size: var(--font-size-sm);
148
+ color: var(--color-text-muted);
149
+ white-space: nowrap;
150
+ }
151
+
152
+ input[data-component="input"] {
153
+ flex: 1;
154
+ min-width: 80px;
155
+ }
156
+ }
157
+ }
158
+
159
+ [data-slot="form-actions"] {
160
+ display: flex;
161
+ gap: var(--space-2);
162
+ margin-top: var(--space-1);
163
+ }
164
+
165
+ [data-slot="form-error"] {
166
+ color: var(--color-danger);
167
+ font-size: var(--font-size-sm);
168
+ line-height: 1.4;
169
+ margin-top: calc(var(--space-1) * -1);
170
+ }
171
+
172
+ [data-slot="model-toggle-label"] {
173
+ position: relative;
174
+ display: inline-block;
175
+ width: 2.5rem;
176
+ height: 1.5rem;
177
+ cursor: pointer;
178
+
179
+ input {
180
+ opacity: 0;
181
+ width: 0;
182
+ height: 0;
183
+ }
184
+
185
+ span {
186
+ position: absolute;
187
+ inset: 0;
188
+ background-color: #ccc;
189
+ border: 1px solid #bbb;
190
+ border-radius: 1.5rem;
191
+ transition: all 0.3s ease;
192
+ cursor: pointer;
193
+
194
+ &::before {
195
+ content: "";
196
+ position: absolute;
197
+ top: 50%;
198
+ left: 0.125rem;
199
+ width: 1.25rem;
200
+ height: 1.25rem;
201
+ background-color: white;
202
+ border: 1px solid #ddd;
203
+ border-radius: 50%;
204
+ transform: translateY(-50%);
205
+ transition: all 0.3s ease;
206
+ }
207
+ }
208
+
209
+ input:checked + span {
210
+ background-color: #21ad0e;
211
+ border-color: #148605;
212
+
213
+ &::before {
214
+ transform: translateX(1rem) translateY(-50%);
215
+ }
216
+ }
217
+
218
+ &:hover span {
219
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
220
+ }
221
+
222
+ input:checked:hover + span {
223
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
224
+ }
225
+
226
+ &:has(input:disabled) {
227
+ cursor: not-allowed;
228
+ }
229
+
230
+ input:disabled + span {
231
+ opacity: 0.5;
232
+ cursor: not-allowed;
233
+ }
234
+ }
235
+ }
236
+
237
+ [data-slot="reload-error"] {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ gap: var(--space-4);
242
+ margin-top: var(--space-4);
243
+
244
+ p {
245
+ color: var(--color-danger);
246
+ font-size: var(--font-size-sm);
247
+ line-height: 1.4;
248
+ margin: 0;
249
+ flex: 1;
250
+ }
251
+
252
+ [data-slot="create-form"] {
253
+ display: flex;
254
+ gap: var(--space-2);
255
+ margin: 0;
256
+ flex-shrink: 0;
257
+ padding: 0;
258
+ border: none;
259
+ }
260
+ }
261
+ }
@@ -0,0 +1,213 @@
1
+ import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
2
+ import { createEffect, Show, createMemo } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { withActor } from "~/context/auth.withActor"
5
+ import { Billing } from "@jonsoc/console-core/billing.js"
6
+ import { Database, eq } from "@jonsoc/console-core/drizzle/index.js"
7
+ import { BillingTable } from "@jonsoc/console-core/schema/billing.sql.js"
8
+ import styles from "./reload-section.module.css"
9
+ import { queryBillingInfo } from "../../common"
10
+
11
+ const reload = action(async (form: FormData) => {
12
+ "use server"
13
+ const workspaceID = form.get("workspaceID")?.toString()
14
+ if (!workspaceID) return { error: "Workspace ID is required" }
15
+ return json(await withActor(() => Billing.reload(), workspaceID), {
16
+ revalidate: queryBillingInfo.key,
17
+ })
18
+ }, "billing.reload")
19
+
20
+ const setReload = action(async (form: FormData) => {
21
+ "use server"
22
+ const workspaceID = form.get("workspaceID")?.toString()
23
+ if (!workspaceID) return { error: "Workspace ID is required" }
24
+ const reloadValue = form.get("reload")?.toString() === "true"
25
+ const amountStr = form.get("reloadAmount")?.toString()
26
+ const triggerStr = form.get("reloadTrigger")?.toString()
27
+
28
+ const reloadAmount = amountStr && amountStr.trim() !== "" ? parseInt(amountStr) : null
29
+ const reloadTrigger = triggerStr && triggerStr.trim() !== "" ? parseInt(triggerStr) : null
30
+
31
+ if (reloadValue) {
32
+ if (reloadAmount === null || reloadAmount < Billing.RELOAD_AMOUNT_MIN)
33
+ return { error: `Reload amount must be at least $${Billing.RELOAD_AMOUNT_MIN}` }
34
+ if (reloadTrigger === null || reloadTrigger < Billing.RELOAD_TRIGGER_MIN)
35
+ return { error: `Balance trigger must be at least $${Billing.RELOAD_TRIGGER_MIN}` }
36
+ }
37
+
38
+ return json(
39
+ await Database.use((tx) =>
40
+ tx
41
+ .update(BillingTable)
42
+ .set({
43
+ reload: reloadValue,
44
+ ...(reloadAmount !== null ? { reloadAmount } : {}),
45
+ ...(reloadTrigger !== null ? { reloadTrigger } : {}),
46
+ ...(reloadValue
47
+ ? {
48
+ reloadError: null,
49
+ timeReloadError: null,
50
+ }
51
+ : {}),
52
+ })
53
+ .where(eq(BillingTable.workspaceID, workspaceID)),
54
+ ),
55
+ { revalidate: queryBillingInfo.key },
56
+ )
57
+ }, "billing.setReload")
58
+
59
+ export function ReloadSection() {
60
+ const params = useParams()
61
+ const billingInfo = createAsync(() => queryBillingInfo(params.id!))
62
+ const setReloadSubmission = useSubmission(setReload)
63
+ const reloadSubmission = useSubmission(reload)
64
+ const [store, setStore] = createStore({
65
+ show: false,
66
+ reload: false,
67
+ reloadAmount: "",
68
+ reloadTrigger: "",
69
+ })
70
+
71
+ const processingFee = createMemo(() => {
72
+ const reloadAmount = billingInfo()?.reloadAmount
73
+ if (!reloadAmount) return "0.00"
74
+ return (((reloadAmount + 0.3) / 0.956) * 0.044 + 0.3).toFixed(2)
75
+ })
76
+
77
+ createEffect(() => {
78
+ if (!setReloadSubmission.pending && setReloadSubmission.result && !(setReloadSubmission.result as any).error) {
79
+ setStore("show", false)
80
+ }
81
+ })
82
+
83
+ function show() {
84
+ while (true) {
85
+ setReloadSubmission.clear()
86
+ if (!setReloadSubmission.result) break
87
+ }
88
+ const info = billingInfo()!
89
+ setStore("show", true)
90
+ setStore("reload", info.reload ? true : true)
91
+ setStore("reloadAmount", info.reloadAmount.toString())
92
+ setStore("reloadTrigger", info.reloadTrigger.toString())
93
+ }
94
+
95
+ function hide() {
96
+ setStore("show", false)
97
+ }
98
+
99
+ return (
100
+ <section class={styles.root}>
101
+ <div data-slot="section-title">
102
+ <h2>Auto Reload</h2>
103
+ <div data-slot="title-row">
104
+ <Show
105
+ when={billingInfo()?.reload}
106
+ fallback={
107
+ <p>
108
+ Auto reload is <b>disabled</b>. Enable to automatically reload when balance is low.
109
+ </p>
110
+ }
111
+ >
112
+ <p>
113
+ Auto reload is <b>enabled</b>. We'll reload <b>${billingInfo()?.reloadAmount}</b> (+${processingFee()}{" "}
114
+ processing fee) when balance reaches <b>${billingInfo()?.reloadTrigger}</b>.
115
+ </p>
116
+ </Show>
117
+ <button data-color="primary" type="button" onClick={() => show()}>
118
+ {billingInfo()?.reload ? "Edit" : "Enable"}
119
+ </button>
120
+ </div>
121
+ </div>
122
+ <Show when={store.show}>
123
+ <form action={setReload} method="post" data-slot="create-form">
124
+ <div data-slot="form-field">
125
+ <label>
126
+ <span data-slot="field-label">Enable Auto Reload</span>
127
+ <div data-slot="toggle-container">
128
+ <label data-slot="model-toggle-label">
129
+ <input
130
+ type="checkbox"
131
+ name="reload"
132
+ value="true"
133
+ checked={store.reload}
134
+ onChange={(e) => setStore("reload", e.currentTarget.checked)}
135
+ />
136
+ <span></span>
137
+ </label>
138
+ </div>
139
+ </label>
140
+ </div>
141
+
142
+ <div data-slot="input-row">
143
+ <div data-slot="input-field">
144
+ <p>Reload $</p>
145
+ <input
146
+ data-component="input"
147
+ name="reloadAmount"
148
+ type="number"
149
+ min={billingInfo()?.reloadAmountMin.toString()}
150
+ step="1"
151
+ value={store.reloadAmount}
152
+ onInput={(e) => setStore("reloadAmount", e.currentTarget.value)}
153
+ placeholder={billingInfo()?.reloadAmount.toString()}
154
+ disabled={!store.reload}
155
+ />
156
+ </div>
157
+ <div data-slot="input-field">
158
+ <p>When balance reaches $</p>
159
+ <input
160
+ data-component="input"
161
+ name="reloadTrigger"
162
+ type="number"
163
+ min={billingInfo()?.reloadTriggerMin.toString()}
164
+ step="1"
165
+ value={store.reloadTrigger}
166
+ onInput={(e) => setStore("reloadTrigger", e.currentTarget.value)}
167
+ placeholder={billingInfo()?.reloadTrigger.toString()}
168
+ disabled={!store.reload}
169
+ />
170
+ </div>
171
+ </div>
172
+
173
+ <Show when={setReloadSubmission.result && (setReloadSubmission.result as any).error}>
174
+ {(err: any) => <div data-slot="form-error">{err()}</div>}
175
+ </Show>
176
+ <input type="hidden" name="workspaceID" value={params.id} />
177
+ <div data-slot="form-actions">
178
+ <button type="button" data-color="ghost" onClick={() => hide()}>
179
+ Cancel
180
+ </button>
181
+ <button type="submit" data-color="primary" disabled={setReloadSubmission.pending}>
182
+ {setReloadSubmission.pending ? "Saving..." : "Save"}
183
+ </button>
184
+ </div>
185
+ </form>
186
+ </Show>
187
+ <Show when={billingInfo()?.reload && billingInfo()?.reloadError}>
188
+ <div data-slot="section-content">
189
+ <div data-slot="reload-error">
190
+ <p>
191
+ Reload failed at{" "}
192
+ {billingInfo()?.timeReloadError!.toLocaleString("en-US", {
193
+ month: "short",
194
+ day: "numeric",
195
+ hour: "numeric",
196
+ minute: "2-digit",
197
+ second: "2-digit",
198
+ })}
199
+ . Reason: {billingInfo()?.reloadError?.replace(/\.$/, "")}. Please update your payment method and try
200
+ again.
201
+ </p>
202
+ <form action={reload} method="post" data-slot="create-form">
203
+ <input type="hidden" name="workspaceID" value={params.id} />
204
+ <button data-color="ghost" type="submit" disabled={reloadSubmission.pending}>
205
+ {reloadSubmission.pending ? "Retrying..." : "Retry"}
206
+ </button>
207
+ </form>
208
+ </div>
209
+ </div>
210
+ </Show>
211
+ </section>
212
+ )
213
+ }