@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,142 @@
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="usage"] {
10
+ display: flex;
11
+ gap: var(--space-6);
12
+ margin-top: var(--space-4);
13
+
14
+ @media (max-width: 40rem) {
15
+ flex-direction: column;
16
+ gap: var(--space-4);
17
+ }
18
+ }
19
+
20
+ [data-slot="usage-item"] {
21
+ flex: 1;
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--space-2);
25
+ }
26
+
27
+ [data-slot="usage-header"] {
28
+ display: flex;
29
+ justify-content: space-between;
30
+ align-items: baseline;
31
+ }
32
+
33
+ [data-slot="usage-label"] {
34
+ font-size: var(--font-size-md);
35
+ font-weight: 500;
36
+ color: var(--color-text);
37
+ }
38
+
39
+ [data-slot="usage-value"] {
40
+ font-size: var(--font-size-sm);
41
+ color: var(--color-text-muted);
42
+ }
43
+
44
+ [data-slot="progress"] {
45
+ height: 8px;
46
+ background-color: var(--color-bg-surface);
47
+ border-radius: var(--border-radius-sm);
48
+ overflow: hidden;
49
+ }
50
+
51
+ [data-slot="progress-bar"] {
52
+ height: 100%;
53
+ background-color: var(--color-accent);
54
+ border-radius: var(--border-radius-sm);
55
+ transition: width 0.3s ease;
56
+ }
57
+
58
+ [data-slot="reset-time"] {
59
+ font-size: var(--font-size-sm);
60
+ color: var(--color-text-muted);
61
+ }
62
+
63
+ [data-slot="setting-row"] {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: var(--space-3);
68
+ margin-top: var(--space-4);
69
+
70
+ p {
71
+ font-size: var(--font-size-sm);
72
+ line-height: 1.5;
73
+ color: var(--color-text-secondary);
74
+ margin: 0;
75
+ }
76
+ }
77
+
78
+ [data-slot="toggle-label"] {
79
+ position: relative;
80
+ display: inline-block;
81
+ width: 2.5rem;
82
+ height: 1.5rem;
83
+ cursor: pointer;
84
+ flex-shrink: 0;
85
+
86
+ input {
87
+ opacity: 0;
88
+ width: 0;
89
+ height: 0;
90
+ }
91
+
92
+ span {
93
+ position: absolute;
94
+ inset: 0;
95
+ background-color: #ccc;
96
+ border: 1px solid #bbb;
97
+ border-radius: 1.5rem;
98
+ transition: all 0.3s ease;
99
+ cursor: pointer;
100
+
101
+ &::before {
102
+ content: "";
103
+ position: absolute;
104
+ top: 50%;
105
+ left: 0.125rem;
106
+ width: 1.25rem;
107
+ height: 1.25rem;
108
+ background-color: white;
109
+ border: 1px solid #ddd;
110
+ border-radius: 50%;
111
+ transform: translateY(-50%);
112
+ transition: all 0.3s ease;
113
+ }
114
+ }
115
+
116
+ input:checked + span {
117
+ background-color: #21ad0e;
118
+ border-color: #148605;
119
+
120
+ &::before {
121
+ transform: translateX(1rem) translateY(-50%);
122
+ }
123
+ }
124
+
125
+ &:hover span {
126
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
127
+ }
128
+
129
+ input:checked:hover + span {
130
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3);
131
+ }
132
+
133
+ &:has(input:disabled) {
134
+ cursor: not-allowed;
135
+ }
136
+
137
+ input:disabled + span {
138
+ opacity: 0.5;
139
+ cursor: not-allowed;
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,269 @@
1
+ import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
2
+ import { createStore } from "solid-js/store"
3
+ import { Show } from "solid-js"
4
+ import { Billing } from "@jonsoc/console-core/billing.js"
5
+ import { Database, eq, and, isNull, sql } from "@jonsoc/console-core/drizzle/index.js"
6
+ import { BillingTable, SubscriptionTable } from "@jonsoc/console-core/schema/billing.sql.js"
7
+ import { Actor } from "@jonsoc/console-core/actor.js"
8
+ import { Black } from "@jonsoc/console-core/black.js"
9
+ import { withActor } from "~/context/auth.withActor"
10
+ import { queryBillingInfo } from "../../common"
11
+ import styles from "./black-section.module.css"
12
+ import waitlistStyles from "./black-waitlist-section.module.css"
13
+
14
+ const querySubscription = query(async (workspaceID: string) => {
15
+ "use server"
16
+ return withActor(async () => {
17
+ const row = await Database.use((tx) =>
18
+ tx
19
+ .select({
20
+ rollingUsage: SubscriptionTable.rollingUsage,
21
+ fixedUsage: SubscriptionTable.fixedUsage,
22
+ timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
23
+ timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
24
+ subscription: BillingTable.subscription,
25
+ })
26
+ .from(BillingTable)
27
+ .innerJoin(SubscriptionTable, eq(SubscriptionTable.workspaceID, BillingTable.workspaceID))
28
+ .where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
29
+ .then((r) => r[0]),
30
+ )
31
+ if (!row?.subscription) return null
32
+
33
+ return {
34
+ plan: row.subscription.plan,
35
+ useBalance: row.subscription.useBalance ?? false,
36
+ rollingUsage: Black.analyzeRollingUsage({
37
+ plan: row.subscription.plan,
38
+ usage: row.rollingUsage ?? 0,
39
+ timeUpdated: row.timeRollingUpdated ?? new Date(),
40
+ }),
41
+ weeklyUsage: Black.analyzeWeeklyUsage({
42
+ plan: row.subscription.plan,
43
+ usage: row.fixedUsage ?? 0,
44
+ timeUpdated: row.timeFixedUpdated ?? new Date(),
45
+ }),
46
+ }
47
+ }, workspaceID)
48
+ }, "subscription.get")
49
+
50
+ function formatResetTime(seconds: number) {
51
+ const days = Math.floor(seconds / 86400)
52
+ if (days >= 1) {
53
+ const hours = Math.floor((seconds % 86400) / 3600)
54
+ return `${days} ${days === 1 ? "day" : "days"} ${hours} ${hours === 1 ? "hour" : "hours"}`
55
+ }
56
+ const hours = Math.floor(seconds / 3600)
57
+ const minutes = Math.floor((seconds % 3600) / 60)
58
+ if (hours >= 1) return `${hours} ${hours === 1 ? "hour" : "hours"} ${minutes} ${minutes === 1 ? "minute" : "minutes"}`
59
+ if (minutes === 0) return "a few seconds"
60
+ return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`
61
+ }
62
+
63
+ const cancelWaitlist = action(async (workspaceID: string) => {
64
+ "use server"
65
+ return json(
66
+ await withActor(async () => {
67
+ await Database.use((tx) =>
68
+ tx
69
+ .update(BillingTable)
70
+ .set({
71
+ subscriptionPlan: null,
72
+ timeSubscriptionBooked: null,
73
+ timeSubscriptionSelected: null,
74
+ })
75
+ .where(eq(BillingTable.workspaceID, workspaceID)),
76
+ )
77
+ return { error: undefined }
78
+ }, workspaceID).catch((e) => ({ error: e.message as string })),
79
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
80
+ )
81
+ }, "cancelWaitlist")
82
+
83
+ const enroll = action(async (workspaceID: string) => {
84
+ "use server"
85
+ return json(
86
+ await withActor(async () => {
87
+ await Billing.subscribe({ seats: 1 })
88
+ return { error: undefined }
89
+ }, workspaceID).catch((e) => ({ error: e.message as string })),
90
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
91
+ )
92
+ }, "enroll")
93
+
94
+ const createSessionUrl = action(async (workspaceID: string, returnUrl: string) => {
95
+ "use server"
96
+ return json(
97
+ await withActor(
98
+ () =>
99
+ Billing.generateSessionUrl({ returnUrl })
100
+ .then((data) => ({ error: undefined, data }))
101
+ .catch((e) => ({
102
+ error: e.message as string,
103
+ data: undefined,
104
+ })),
105
+ workspaceID,
106
+ ),
107
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
108
+ )
109
+ }, "sessionUrl")
110
+
111
+ const setUseBalance = action(async (form: FormData) => {
112
+ "use server"
113
+ const workspaceID = form.get("workspaceID")?.toString()
114
+ if (!workspaceID) return { error: "Workspace ID is required" }
115
+ const useBalance = form.get("useBalance")?.toString() === "true"
116
+
117
+ return json(
118
+ await withActor(async () => {
119
+ await Database.use((tx) =>
120
+ tx
121
+ .update(BillingTable)
122
+ .set({
123
+ subscription: useBalance
124
+ ? sql`JSON_SET(subscription, '$.useBalance', true)`
125
+ : sql`JSON_REMOVE(subscription, '$.useBalance')`,
126
+ })
127
+ .where(eq(BillingTable.workspaceID, workspaceID)),
128
+ )
129
+ return { error: undefined }
130
+ }, workspaceID).catch((e) => ({ error: e.message as string })),
131
+ { revalidate: [queryBillingInfo.key, querySubscription.key] },
132
+ )
133
+ }, "setUseBalance")
134
+
135
+ export function BlackSection() {
136
+ const params = useParams()
137
+ const billing = createAsync(() => queryBillingInfo(params.id!))
138
+ const subscription = createAsync(() => querySubscription(params.id!))
139
+ const sessionAction = useAction(createSessionUrl)
140
+ const sessionSubmission = useSubmission(createSessionUrl)
141
+ const cancelAction = useAction(cancelWaitlist)
142
+ const cancelSubmission = useSubmission(cancelWaitlist)
143
+ const enrollAction = useAction(enroll)
144
+ const enrollSubmission = useSubmission(enroll)
145
+ const useBalanceSubmission = useSubmission(setUseBalance)
146
+ const [store, setStore] = createStore({
147
+ sessionRedirecting: false,
148
+ cancelled: false,
149
+ enrolled: false,
150
+ })
151
+
152
+ async function onClickSession() {
153
+ const result = await sessionAction(params.id!, window.location.href)
154
+ if (result.data) {
155
+ setStore("sessionRedirecting", true)
156
+ window.location.href = result.data
157
+ }
158
+ }
159
+
160
+ async function onClickCancel() {
161
+ const result = await cancelAction(params.id!)
162
+ if (!result.error) {
163
+ setStore("cancelled", true)
164
+ }
165
+ }
166
+
167
+ async function onClickEnroll() {
168
+ const result = await enrollAction(params.id!)
169
+ if (!result.error) {
170
+ setStore("enrolled", true)
171
+ }
172
+ }
173
+
174
+ return (
175
+ <>
176
+ <Show when={subscription()}>
177
+ {(sub) => (
178
+ <section class={styles.root}>
179
+ <div data-slot="section-title">
180
+ <h2>Subscription</h2>
181
+ <div data-slot="title-row">
182
+ <p>You are subscribed to JonsOC Black for ${sub().plan} per month.</p>
183
+ <button
184
+ data-color="primary"
185
+ disabled={sessionSubmission.pending || store.sessionRedirecting}
186
+ onClick={onClickSession}
187
+ >
188
+ {sessionSubmission.pending || store.sessionRedirecting ? "Loading..." : "Manage Subscription"}
189
+ </button>
190
+ </div>
191
+ </div>
192
+ <div data-slot="usage">
193
+ <div data-slot="usage-item">
194
+ <div data-slot="usage-header">
195
+ <span data-slot="usage-label">5-hour Usage</span>
196
+ <span data-slot="usage-value">{sub().rollingUsage.usagePercent}%</span>
197
+ </div>
198
+ <div data-slot="progress">
199
+ <div data-slot="progress-bar" style={{ width: `${sub().rollingUsage.usagePercent}%` }} />
200
+ </div>
201
+ <span data-slot="reset-time">Resets in {formatResetTime(sub().rollingUsage.resetInSec)}</span>
202
+ </div>
203
+ <div data-slot="usage-item">
204
+ <div data-slot="usage-header">
205
+ <span data-slot="usage-label">Weekly Usage</span>
206
+ <span data-slot="usage-value">{sub().weeklyUsage.usagePercent}%</span>
207
+ </div>
208
+ <div data-slot="progress">
209
+ <div data-slot="progress-bar" style={{ width: `${sub().weeklyUsage.usagePercent}%` }} />
210
+ </div>
211
+ <span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
212
+ </div>
213
+ </div>
214
+ <form action={setUseBalance} method="post" data-slot="setting-row">
215
+ <p>Use your available balance after reaching the usage limits</p>
216
+ <input type="hidden" name="workspaceID" value={params.id} />
217
+ <input type="hidden" name="useBalance" value={sub().useBalance ? "false" : "true"} />
218
+ <label data-slot="toggle-label">
219
+ <input
220
+ type="checkbox"
221
+ checked={sub().useBalance}
222
+ disabled={useBalanceSubmission.pending}
223
+ onChange={(e) => e.currentTarget.form?.requestSubmit()}
224
+ />
225
+ <span></span>
226
+ </label>
227
+ </form>
228
+ </section>
229
+ )}
230
+ </Show>
231
+ <Show when={billing()?.timeSubscriptionBooked}>
232
+ <section class={waitlistStyles.root}>
233
+ <div data-slot="section-title">
234
+ <h2>Waitlist</h2>
235
+ <div data-slot="title-row">
236
+ <p>
237
+ {billing()?.timeSubscriptionSelected
238
+ ? `We're ready to enroll you into the $${billing()?.subscriptionPlan} per month JonsOC Black plan.`
239
+ : `You are on the waitlist for the $${billing()?.subscriptionPlan} per month JonsOC Black plan.`}
240
+ </p>
241
+ <button
242
+ data-color="danger"
243
+ disabled={cancelSubmission.pending || store.cancelled}
244
+ onClick={onClickCancel}
245
+ >
246
+ {cancelSubmission.pending ? "Leaving..." : store.cancelled ? "Left" : "Leave Waitlist"}
247
+ </button>
248
+ </div>
249
+ </div>
250
+ <Show when={billing()?.timeSubscriptionSelected}>
251
+ <div data-slot="enroll-section">
252
+ <button
253
+ data-slot="enroll-button"
254
+ data-color="primary"
255
+ disabled={enrollSubmission.pending || store.enrolled}
256
+ onClick={onClickEnroll}
257
+ >
258
+ {enrollSubmission.pending ? "Enrolling..." : store.enrolled ? "Enrolled" : "Enroll"}
259
+ </button>
260
+ <p data-slot="enroll-note">
261
+ When you click Enroll, your subscription starts immediately and your card will be charged.
262
+ </p>
263
+ </div>
264
+ </Show>
265
+ </section>
266
+ </Show>
267
+ </>
268
+ )
269
+ }
@@ -0,0 +1,23 @@
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="enroll-section"] {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--space-3);
13
+ }
14
+
15
+ [data-slot="enroll-button"] {
16
+ align-self: flex-start;
17
+ }
18
+
19
+ [data-slot="enroll-note"] {
20
+ font-size: var(--font-size-sm);
21
+ color: var(--color-text-muted);
22
+ }
23
+ }
@@ -0,0 +1,32 @@
1
+ import { MonthlyLimitSection } from "./monthly-limit-section"
2
+ import { BillingSection } from "./billing-section"
3
+ import { ReloadSection } from "./reload-section"
4
+ import { PaymentSection } from "./payment-section"
5
+ import { BlackSection } from "./black-section"
6
+ import { Show } from "solid-js"
7
+ import { createAsync, useParams } from "@solidjs/router"
8
+ import { queryBillingInfo, querySessionInfo } from "../../common"
9
+
10
+ export default function () {
11
+ const params = useParams()
12
+ const sessionInfo = createAsync(() => querySessionInfo(params.id!))
13
+ const billingInfo = createAsync(() => queryBillingInfo(params.id!))
14
+
15
+ return (
16
+ <div data-page="workspace-[id]">
17
+ <div data-slot="sections">
18
+ <Show when={sessionInfo()?.isAdmin}>
19
+ <Show when={billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked}>
20
+ <BlackSection />
21
+ </Show>
22
+ <BillingSection />
23
+ <Show when={billingInfo()?.customerID}>
24
+ <ReloadSection />
25
+ <MonthlyLimitSection />
26
+ <PaymentSection />
27
+ </Show>
28
+ </Show>
29
+ </div>
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,96 @@
1
+ .root {
2
+ [data-slot="balance"] {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: var(--space-3);
6
+ padding: var(--space-4);
7
+ border: 1px solid var(--color-border);
8
+ border-radius: var(--border-radius-sm);
9
+ min-width: 15rem;
10
+ width: fit-content;
11
+
12
+ @media (max-width: 30rem) {
13
+ width: 100%;
14
+ }
15
+
16
+ [data-slot="amount"] {
17
+ padding: var(--space-3-5) var(--space-4);
18
+ background-color: var(--color-bg-surface);
19
+ border-radius: var(--border-radius-sm);
20
+ display: flex;
21
+ align-items: baseline;
22
+ gap: var(--space-1);
23
+ justify-content: flex-end;
24
+
25
+ [data-slot="currency"] {
26
+ position: relative;
27
+ bottom: 2px;
28
+ font-size: var(--font-size-lg);
29
+ color: var(--color-text-muted);
30
+ font-weight: 400;
31
+ }
32
+
33
+ [data-slot="value"] {
34
+ font-size: var(--font-size-3xl);
35
+ font-weight: 500;
36
+ color: var(--color-text);
37
+ }
38
+ }
39
+
40
+ [data-slot="create-form"] {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--space-3);
44
+ margin-top: var(--space-1);
45
+
46
+ [data-slot="input-container"] {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: var(--space-1);
50
+ }
51
+
52
+ @media (max-width: 30rem) {
53
+ gap: var(--space-2);
54
+ }
55
+
56
+ input {
57
+ flex: 1;
58
+ padding: var(--space-2) var(--space-3);
59
+ border: 1px solid var(--color-border);
60
+ border-radius: var(--border-radius-sm);
61
+ background-color: var(--color-bg);
62
+ color: var(--color-text);
63
+ font-size: var(--font-size-sm);
64
+ font-family: var(--font-mono);
65
+
66
+ &:focus {
67
+ outline: none;
68
+ border-color: var(--color-accent);
69
+ }
70
+
71
+ &::placeholder {
72
+ color: var(--color-text-disabled);
73
+ }
74
+ }
75
+
76
+ [data-slot="form-actions"] {
77
+ display: flex;
78
+ gap: var(--space-2);
79
+ justify-content: flex-end;
80
+ }
81
+
82
+ [data-slot="form-error"] {
83
+ color: var(--color-danger);
84
+ font-size: var(--font-size-sm);
85
+ line-height: 1.4;
86
+ }
87
+ }
88
+ }
89
+
90
+ [data-slot="usage-status"] {
91
+ font-size: var(--font-size-sm);
92
+ color: var(--color-text-secondary);
93
+ margin: 0;
94
+ line-height: 1.4;
95
+ }
96
+ }
@@ -0,0 +1,133 @@
1
+ import { json, action, useParams, createAsync, useSubmission } from "@solidjs/router"
2
+ import { createEffect, Show } 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 styles from "./monthly-limit-section.module.css"
7
+ import { queryBillingInfo } from "../../common"
8
+
9
+ const setMonthlyLimit = action(async (form: FormData) => {
10
+ "use server"
11
+ const limit = form.get("limit")?.toString()
12
+ if (!limit) return { error: "Limit is required." }
13
+ const numericLimit = parseInt(limit)
14
+ if (numericLimit < 0) return { error: "Set a valid monthly limit." }
15
+ const workspaceID = form.get("workspaceID")?.toString()
16
+ if (!workspaceID) return { error: "Workspace ID is required." }
17
+ return json(
18
+ await withActor(
19
+ () =>
20
+ Billing.setMonthlyLimit(numericLimit)
21
+ .then((data) => ({ error: undefined, data }))
22
+ .catch((e) => ({ error: e.message as string })),
23
+ workspaceID,
24
+ ),
25
+ { revalidate: queryBillingInfo.key },
26
+ )
27
+ }, "billing.setMonthlyLimit")
28
+
29
+ export function MonthlyLimitSection() {
30
+ const params = useParams()
31
+ const submission = useSubmission(setMonthlyLimit)
32
+ const [store, setStore] = createStore({ show: false })
33
+ const billingInfo = createAsync(() => queryBillingInfo(params.id!))
34
+
35
+ let input: HTMLInputElement
36
+
37
+ createEffect(() => {
38
+ if (!submission.pending && submission.result && !submission.result.error) {
39
+ hide()
40
+ }
41
+ })
42
+
43
+ function show() {
44
+ // submission.clear() does not clear the result in some cases, ie.
45
+ // 1. Create key with empty name => error shows
46
+ // 2. Put in a key name and creates the key => form hides
47
+ // 3. Click add key button again => form shows with the same error if
48
+ // submission.clear() is called only once
49
+ while (true) {
50
+ submission.clear()
51
+ if (!submission.result) break
52
+ }
53
+ setStore("show", true)
54
+ input.focus()
55
+ }
56
+
57
+ function hide() {
58
+ setStore("show", false)
59
+ }
60
+
61
+ return (
62
+ <section class={styles.root}>
63
+ <div data-slot="section-title">
64
+ <h2>Monthly Limit</h2>
65
+ <p>Set a monthly usage limit for your account.</p>
66
+ </div>
67
+ <div data-slot="section-content">
68
+ <div data-slot="balance">
69
+ <div data-slot="amount">
70
+ {billingInfo()?.monthlyLimit ? <span data-slot="currency">$</span> : null}
71
+ <span data-slot="value">{billingInfo()?.monthlyLimit ?? "-"}</span>
72
+ </div>
73
+ <Show
74
+ when={!store.show}
75
+ fallback={
76
+ <form action={setMonthlyLimit} method="post" data-slot="create-form">
77
+ <div data-slot="input-container">
78
+ <input
79
+ required
80
+ ref={(r) => (input = r)}
81
+ data-component="input"
82
+ name="limit"
83
+ type="number"
84
+ placeholder="50"
85
+ />
86
+ <Show when={submission.result && submission.result.error}>
87
+ {(err) => <div data-slot="form-error">{err()}</div>}
88
+ </Show>
89
+ </div>
90
+ <input type="hidden" name="workspaceID" value={params.id} />
91
+ <div data-slot="form-actions">
92
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
93
+ Cancel
94
+ </button>
95
+ <button type="submit" data-color="primary" disabled={submission.pending}>
96
+ {submission.pending ? "Setting..." : "Set"}
97
+ </button>
98
+ </div>
99
+ </form>
100
+ }
101
+ >
102
+ <button data-color="primary" onClick={() => show()}>
103
+ {billingInfo()?.monthlyLimit ? "Edit Limit" : "Set Limit"}
104
+ </button>
105
+ </Show>
106
+ </div>
107
+ <Show when={billingInfo()?.monthlyLimit} fallback={<p data-slot="usage-status">No usage limit set.</p>}>
108
+ <p data-slot="usage-status">
109
+ Current usage for {new Date().toLocaleDateString("en-US", { month: "long", timeZone: "UTC" })} is $
110
+ {(() => {
111
+ const dateLastUsed = billingInfo()?.timeMonthlyUsageUpdated
112
+ if (!dateLastUsed) return "0"
113
+
114
+ const current = new Date().toLocaleDateString("en-US", {
115
+ year: "numeric",
116
+ month: "long",
117
+ timeZone: "UTC",
118
+ })
119
+ const lastUsed = dateLastUsed.toLocaleDateString("en-US", {
120
+ year: "numeric",
121
+ month: "long",
122
+ timeZone: "UTC",
123
+ })
124
+ if (current !== lastUsed) return "0"
125
+ return ((billingInfo()?.monthlyUsage ?? 0) / 100000000).toFixed(2)
126
+ })()}
127
+ .
128
+ </p>
129
+ </Show>
130
+ </div>
131
+ </section>
132
+ )
133
+ }