@tscircuit/fake-snippets 0.0.82 → 0.0.84

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 (44) hide show
  1. package/README.md +5 -2
  2. package/bun-tests/fake-snippets-api/routes/ai_reviews/create.test.ts +12 -0
  3. package/bun-tests/fake-snippets-api/routes/ai_reviews/get.test.ts +16 -0
  4. package/bun-tests/fake-snippets-api/routes/ai_reviews/list.test.ts +14 -0
  5. package/bun-tests/fake-snippets-api/routes/ai_reviews/process_review.test.ts +16 -0
  6. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +3 -3
  7. package/bun.lock +26 -37
  8. package/dist/bundle.js +590 -427
  9. package/dist/index.d.ts +83 -11
  10. package/dist/index.js +50 -2
  11. package/dist/schema.d.ts +116 -15
  12. package/dist/schema.js +17 -2
  13. package/fake-snippets-api/lib/db/db-client.ts +40 -0
  14. package/fake-snippets-api/lib/db/schema.ts +17 -1
  15. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +14 -1
  16. package/fake-snippets-api/routes/api/_fake/ai_reviews/process_review.ts +31 -0
  17. package/fake-snippets-api/routes/api/ai_reviews/create.ts +22 -0
  18. package/fake-snippets-api/routes/api/ai_reviews/get.ts +24 -0
  19. package/fake-snippets-api/routes/api/ai_reviews/list.ts +14 -0
  20. package/fake-snippets-api/routes/api/package_releases/get.ts +11 -3
  21. package/fake-snippets-api/routes/api/package_releases/list.ts +8 -1
  22. package/package.json +4 -3
  23. package/src/App.tsx +0 -2
  24. package/src/ContextProviders.tsx +1 -1
  25. package/src/components/Header2.tsx +8 -18
  26. package/src/components/PackageBuildsPage/package-build-header.tsx +14 -2
  27. package/src/components/SearchComponent.tsx +46 -8
  28. package/src/components/ViewPackagePage/hooks/use-toast.tsx +70 -0
  29. package/src/components/ViewSnippetHeader.tsx +9 -6
  30. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -10
  31. package/src/components/dialogs/{import-snippet-dialog.tsx → import-package-dialog.tsx} +25 -24
  32. package/src/components/package-port/CodeEditorHeader.tsx +7 -6
  33. package/src/components/ui/toaster.tsx +1 -33
  34. package/src/hooks/use-current-package-release.ts +10 -1
  35. package/src/hooks/use-fork-package-mutation.ts +4 -3
  36. package/src/hooks/use-package-release.ts +15 -14
  37. package/src/hooks/use-sign-in.ts +10 -8
  38. package/src/hooks/use-toast.tsx +50 -169
  39. package/src/hooks/useFileManagement.ts +74 -12
  40. package/src/hooks/useForkPackageMutation.ts +2 -1
  41. package/src/hooks/useForkSnippetMutation.ts +2 -1
  42. package/src/pages/authorize.tsx +164 -8
  43. package/src/pages/view-package.tsx +1 -0
  44. package/src/components/ViewPackagePage/hooks/use-toast.ts +0 -191
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState, useCallback } from "react"
1
+ import { useEffect, useMemo, useState, useCallback, useRef } from "react"
2
2
  import { isValidFileName } from "@/lib/utils/isValidFileName"
3
3
  import {
4
4
  DEFAULT_CODE,
@@ -6,11 +6,7 @@ import {
6
6
  PackageFile,
7
7
  } from "../components/package-port/CodeAndPreview"
8
8
  import { Package } from "fake-snippets-api/lib/db/schema"
9
- import {
10
- usePackageFile,
11
- usePackageFileById,
12
- usePackageFiles,
13
- } from "./use-package-files"
9
+ import { usePackageFiles } from "./use-package-files"
14
10
  import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
15
11
  import { usePackageFilesLoader } from "./usePackageFilesLoader"
16
12
  import { useGlobalStore } from "./use-global-store"
@@ -19,6 +15,7 @@ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
19
15
  import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
20
16
  import { useCreatePackageMutation } from "./use-create-package-mutation"
21
17
  import { findTargetFile } from "@/lib/utils/findTargetFile"
18
+ import { createSnippetUrl } from "@tscircuit/create-snippet-url"
22
19
 
23
20
  export interface ICreateFileProps {
24
21
  newFileName: string
@@ -55,17 +52,19 @@ export function useFileManagement({
55
52
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
56
53
  const loggedInUser = useGlobalStore((s) => s.session)
57
54
  const { toast } = useToast()
55
+ const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
58
56
  const {
59
57
  data: packageFilesWithContent,
60
58
  isLoading: isLoadingPackageFilesWithContent,
61
59
  } = usePackageFilesLoader(currentPackage)
62
60
  const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
63
61
  usePackageFiles(currentPackage?.latest_package_release_id)
64
-
65
62
  const initialCodeContent = useMemo(() => {
66
63
  return (
67
- templateCode ??
68
- (decodeUrlHashToText(window.location.toString()) || DEFAULT_CODE)
64
+ (!!decodeUrlHashToText(window.location.toString()) &&
65
+ decodeUrlHashToText(window.location.toString()) !== ""
66
+ ? decodeUrlHashToText(window.location.toString())
67
+ : templateCode) || DEFAULT_CODE
69
68
  )
70
69
  }, [templateCode, currentPackage])
71
70
  const manualEditsFileContent = useMemo(() => {
@@ -193,7 +192,8 @@ export function useFileManagement({
193
192
  { path: newFileName, content: "" },
194
193
  ]
195
194
  setLocalFiles(updatedFiles)
196
- onFileSelect(newFileName)
195
+ // immediately select the newly created file
196
+ setCurrentFile(newFileName)
197
197
  return {
198
198
  newFileCreated: true,
199
199
  }
@@ -233,14 +233,76 @@ export function useFileManagement({
233
233
  })
234
234
  }
235
235
 
236
+ const saveToUrl = useCallback(
237
+ (files: PackageFile[]) => {
238
+ if (isLoggedIn || !files.length) return
239
+
240
+ if (debounceTimeoutRef.current) {
241
+ clearTimeout(debounceTimeoutRef.current)
242
+ }
243
+
244
+ debounceTimeoutRef.current = setTimeout(() => {
245
+ try {
246
+ const mainFile =
247
+ files.find((f) => f.path === currentFile) ||
248
+ files.find((f) => f.path === "index.tsx") ||
249
+ files[0]
250
+
251
+ if (mainFile.content.length > 50000) return
252
+
253
+ const snippetUrl = createSnippetUrl(mainFile.content)
254
+ if (typeof snippetUrl !== "string") return
255
+
256
+ const currentUrl = new URL(window.location.href)
257
+ const urlParts = snippetUrl.split("#")
258
+
259
+ if (urlParts.length > 1 && urlParts[1]) {
260
+ const newHash = urlParts[1]
261
+ if (newHash.length > 8000) return
262
+
263
+ currentUrl.hash = newHash
264
+ const finalUrl = currentUrl.toString()
265
+
266
+ if (finalUrl.length <= 32000) {
267
+ window.history.replaceState(null, "", finalUrl)
268
+ }
269
+ }
270
+ } catch (error) {
271
+ console.warn("Failed to save code to URL:", error)
272
+ }
273
+ }, 1000)
274
+ },
275
+ [isLoggedIn, currentFile],
276
+ )
277
+
278
+ useEffect(() => {
279
+ if (!isLoggedIn && localFiles.length > 0) {
280
+ saveToUrl(localFiles)
281
+ }
282
+
283
+ return () => {
284
+ if (debounceTimeoutRef.current) {
285
+ clearTimeout(debounceTimeoutRef.current)
286
+ }
287
+ }
288
+ }, [localFiles, saveToUrl, isLoggedIn])
289
+
236
290
  const saveFiles = () => {
237
291
  if (!isLoggedIn) {
292
+ // For non-logged-in users, trigger immediate URL save
293
+ if (debounceTimeoutRef.current) {
294
+ clearTimeout(debounceTimeoutRef.current)
295
+ }
296
+ saveToUrl(localFiles)
297
+
238
298
  toast({
239
- title: "Not Logged In",
240
- description: "You must be logged in to save your package.",
299
+ title: "Code Saved to URL",
300
+ description:
301
+ "Your code has been saved to the URL. Bookmark this page to access your code later.",
241
302
  })
242
303
  return
243
304
  }
305
+
244
306
  if (!currentPackage) {
245
307
  openNewPackageSaveDialog()
246
308
  return
@@ -38,9 +38,10 @@ export const useForkPackageMutation = ({
38
38
  },
39
39
  onError: (error: any) => {
40
40
  console.error("Error forking package:", error)
41
+ const message = error?.data?.error?.message
41
42
  toast({
42
43
  title: "Error",
43
- description: "Failed to fork package. Please try again.",
44
+ description: message || "Failed to fork package. Please try again.",
44
45
  variant: "destructive",
45
46
  })
46
47
  },
@@ -41,9 +41,10 @@ export const useForkSnippetMutation = ({
41
41
  },
42
42
  onError: (error: any) => {
43
43
  console.error("Error forking snippet:", error)
44
+ const message = error?.data?.error?.message
44
45
  toast({
45
46
  title: "Error",
46
- description: "Failed to fork snippet. Please try again.",
47
+ description: message || "Failed to fork snippet. Please try again.",
47
48
  variant: "destructive",
48
49
  })
49
50
  },
@@ -6,6 +6,33 @@ import { useEffect, useState } from "react"
6
6
  import * as jose from "jose"
7
7
  import Footer from "@/components/Footer"
8
8
  import { useLocation } from "wouter"
9
+ import { useIsUsingFakeApi } from "@/hooks/use-is-using-fake-api"
10
+ import { CheckCircle, AlertCircle, Loader2 } from "lucide-react"
11
+
12
+ const handleRedirect = (
13
+ redirect: string | null,
14
+ fallbackLocation: () => void,
15
+ ) => {
16
+ if (redirect) {
17
+ try {
18
+ const decodedRedirect = decodeURIComponent(redirect)
19
+
20
+ if (decodedRedirect.startsWith("/")) {
21
+ window.location.href = decodedRedirect
22
+ return
23
+ }
24
+
25
+ const redirectUrl = new URL(decodedRedirect)
26
+ if (redirectUrl.origin === window.location.origin) {
27
+ window.location.href = redirectUrl.href
28
+ return
29
+ }
30
+ } catch (e) {
31
+ console.warn("Invalid redirect URL:", redirect)
32
+ }
33
+ }
34
+ fallbackLocation()
35
+ }
9
36
 
10
37
  const AuthenticatePageInnerContent = () => {
11
38
  const [location, setLocation] = useLocation()
@@ -13,32 +40,161 @@ const AuthenticatePageInnerContent = () => {
13
40
  const [message, setMessage] = useState("logging you in...")
14
41
  const searchParams = new URLSearchParams(window.location.search.split("?")[1])
15
42
  const session_token = searchParams.get("session_token")
43
+ const redirect = searchParams.get("redirect")
44
+ const isUsingFakeApi = useIsUsingFakeApi()
45
+
46
+ const isError = message.includes("error") || message.includes("couldn't")
47
+ const isSuccess =
48
+ message.includes("success") || message.includes("redirecting")
49
+
16
50
  useEffect(() => {
17
51
  async function login() {
52
+ if (isUsingFakeApi) {
53
+ setSession({
54
+ account_id: "account-1234",
55
+ github_username: "testuser",
56
+ token: "1234",
57
+ session_id: "session-1234",
58
+ })
59
+
60
+ handleRedirect(redirect, () => setLocation("/"))
61
+ return
62
+ }
18
63
  if (!session_token) {
19
- setMessage("couldn't log in - no token")
64
+ setMessage("couldn't log in - no token provided")
20
65
  return
21
66
  }
22
-
23
67
  if (session_token) {
24
68
  const decodedToken = jose.decodeJwt(session_token)
25
69
  setSession({
26
70
  ...(decodedToken as any),
27
71
  token: session_token,
28
72
  })
29
- setLocation("/")
73
+ setMessage("success! redirecting you now...")
74
+ setTimeout(() => {
75
+ handleRedirect(redirect, () => setLocation("/"))
76
+ }, 1000)
30
77
  return
31
78
  }
32
79
  }
33
80
  login().catch((e) => {
34
- setMessage(`error logging you in\n\n${e.toString()}`)
81
+ setMessage(`error logging you in: ${e.message || e.toString()}`)
35
82
  })
36
- }, [session_token])
83
+ }, [session_token, redirect])
37
84
 
38
85
  return (
39
- <div className="bg-white p-8 min-h-screen">
40
- <div>Authentication Redirect</div>
41
- <pre>{message}</pre>
86
+ <div className="min-h-screen bg-white flex items-center justify-center px-4">
87
+ <div className="w-full max-w-md">
88
+ <div className="bg-gray-50/20 rounded-2xl shadow-xl border border-gray-200 p-8">
89
+ <div className="text-center">
90
+ <div className="mb-6">
91
+ {isError ? (
92
+ <div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
93
+ <AlertCircle className="h-8 w-8 text-red-500" />
94
+ </div>
95
+ ) : isSuccess ? (
96
+ <div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
97
+ <CheckCircle className="h-8 w-8 text-green-500" />
98
+ </div>
99
+ ) : (
100
+ <div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
101
+ <Loader2 className="h-8 w-8 text-blue-500 animate-spin" />
102
+ </div>
103
+ )}
104
+ </div>
105
+
106
+ {/* Title */}
107
+ <h1 className="text-2xl font-semibold text-gray-900 mb-3">
108
+ {isError
109
+ ? "Authentication Failed"
110
+ : isSuccess
111
+ ? "Success!"
112
+ : "Signing You In"}
113
+ </h1>
114
+
115
+ {/* Message */}
116
+ <div className="text-gray-600">
117
+ {isError ? (
118
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-left">
119
+ <p className="text-red-800 font-medium mb-2">
120
+ Authentication Error
121
+ </p>
122
+ <p className="text-red-700 text-sm break-words">{message}</p>
123
+ <div className="mt-4 pt-3 border-t border-red-200">
124
+ <p className="text-red-600 text-xs">
125
+ Please try signing in again or contact support if the
126
+ problem persists.
127
+ </p>
128
+ </div>
129
+ </div>
130
+ ) : isSuccess ? (
131
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4">
132
+ <p className="text-green-800 font-medium">
133
+ Authentication successful!
134
+ </p>
135
+ <p className="text-green-700 text-sm mt-1">
136
+ Redirecting you now...
137
+ </p>
138
+ </div>
139
+ ) : (
140
+ <div>
141
+ <p className="text-gray-600 mb-4">
142
+ Please wait while we authenticate your account and redirect
143
+ you to your destination.
144
+ </p>
145
+ <div className="flex items-center justify-center text-sm text-gray-500">
146
+ <div className="flex space-x-1">
147
+ <div className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"></div>
148
+ <div
149
+ className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"
150
+ style={{ animationDelay: "0.1s" }}
151
+ ></div>
152
+ <div
153
+ className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"
154
+ style={{ animationDelay: "0.2s" }}
155
+ ></div>
156
+ </div>
157
+ <span className="ml-2">Processing</span>
158
+ </div>
159
+ </div>
160
+ )}
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ {/* Footer info */}
166
+ <div className="text-center mt-6 text-xs text-gray-500">
167
+ If you encounter any issues, please report them on our{" "}
168
+ <a
169
+ href="https://github.com/tscircuit/tscircuit/issues"
170
+ className="text-blue-500 underline"
171
+ >
172
+ GitHub Issues page
173
+ </a>
174
+ .
175
+ </div>
176
+ </div>
177
+
178
+ <style
179
+ dangerouslySetInnerHTML={{
180
+ __html: `
181
+ @keyframes pulse-loading {
182
+ 0% {
183
+ width: 0%;
184
+ }
185
+ 50% {
186
+ width: 60%;
187
+ }
188
+ 100% {
189
+ width: 100%;
190
+ }
191
+ }
192
+ .animate-pulse-loading {
193
+ animation: pulse-loading 2s ease-in-out infinite;
194
+ }
195
+ `,
196
+ }}
197
+ />
42
198
  </div>
43
199
  )
44
200
  }
@@ -24,6 +24,7 @@ export const ViewPackagePage = () => {
24
24
  } = usePackageRelease({
25
25
  is_latest: true,
26
26
  package_name: `${author}/${packageName}`,
27
+ include_ai_review: true,
27
28
  })
28
29
 
29
30
  const { data: packageFiles } = usePackageFiles(
@@ -1,191 +0,0 @@
1
- "use client"
2
-
3
- // Inspired by react-hot-toast library
4
- import * as React from "react"
5
-
6
- import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
7
-
8
- const TOAST_LIMIT = 1
9
- const TOAST_REMOVE_DELAY = 1000000
10
-
11
- type ToasterToast = ToastProps & {
12
- id: string
13
- title?: React.ReactNode
14
- description?: React.ReactNode
15
- action?: ToastActionElement
16
- }
17
-
18
- const actionTypes = {
19
- ADD_TOAST: "ADD_TOAST",
20
- UPDATE_TOAST: "UPDATE_TOAST",
21
- DISMISS_TOAST: "DISMISS_TOAST",
22
- REMOVE_TOAST: "REMOVE_TOAST",
23
- } as const
24
-
25
- let count = 0
26
-
27
- function genId() {
28
- count = (count + 1) % Number.MAX_SAFE_INTEGER
29
- return count.toString()
30
- }
31
-
32
- type ActionType = typeof actionTypes
33
-
34
- type Action =
35
- | {
36
- type: ActionType["ADD_TOAST"]
37
- toast: ToasterToast
38
- }
39
- | {
40
- type: ActionType["UPDATE_TOAST"]
41
- toast: Partial<ToasterToast>
42
- }
43
- | {
44
- type: ActionType["DISMISS_TOAST"]
45
- toastId?: ToasterToast["id"]
46
- }
47
- | {
48
- type: ActionType["REMOVE_TOAST"]
49
- toastId?: ToasterToast["id"]
50
- }
51
-
52
- interface State {
53
- toasts: ToasterToast[]
54
- }
55
-
56
- const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
57
-
58
- const addToRemoveQueue = (toastId: string) => {
59
- if (toastTimeouts.has(toastId)) {
60
- return
61
- }
62
-
63
- const timeout = setTimeout(() => {
64
- toastTimeouts.delete(toastId)
65
- dispatch({
66
- type: "REMOVE_TOAST",
67
- toastId: toastId,
68
- })
69
- }, TOAST_REMOVE_DELAY)
70
-
71
- toastTimeouts.set(toastId, timeout)
72
- }
73
-
74
- export const reducer = (state: State, action: Action): State => {
75
- switch (action.type) {
76
- case "ADD_TOAST":
77
- return {
78
- ...state,
79
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80
- }
81
-
82
- case "UPDATE_TOAST":
83
- return {
84
- ...state,
85
- toasts: state.toasts.map((t) =>
86
- t.id === action.toast.id ? { ...t, ...action.toast } : t,
87
- ),
88
- }
89
-
90
- case "DISMISS_TOAST": {
91
- const { toastId } = action
92
-
93
- // ! Side effects ! - This could be extracted into a dismissToast() action,
94
- // but I'll keep it here for simplicity
95
- if (toastId) {
96
- addToRemoveQueue(toastId)
97
- } else {
98
- state.toasts.forEach((toast) => {
99
- addToRemoveQueue(toast.id)
100
- })
101
- }
102
-
103
- return {
104
- ...state,
105
- toasts: state.toasts.map((t) =>
106
- t.id === toastId || toastId === undefined
107
- ? {
108
- ...t,
109
- open: false,
110
- }
111
- : t,
112
- ),
113
- }
114
- }
115
- case "REMOVE_TOAST":
116
- if (action.toastId === undefined) {
117
- return {
118
- ...state,
119
- toasts: [],
120
- }
121
- }
122
- return {
123
- ...state,
124
- toasts: state.toasts.filter((t) => t.id !== action.toastId),
125
- }
126
- }
127
- }
128
-
129
- const listeners: Array<(state: State) => void> = []
130
-
131
- let memoryState: State = { toasts: [] }
132
-
133
- function dispatch(action: Action) {
134
- memoryState = reducer(memoryState, action)
135
- listeners.forEach((listener) => {
136
- listener(memoryState)
137
- })
138
- }
139
-
140
- type Toast = Omit<ToasterToast, "id">
141
-
142
- function toast({ ...props }: Toast) {
143
- const id = genId()
144
-
145
- const update = (props: ToasterToast) =>
146
- dispatch({
147
- type: "UPDATE_TOAST",
148
- toast: { ...props, id },
149
- })
150
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
151
-
152
- dispatch({
153
- type: "ADD_TOAST",
154
- toast: {
155
- ...props,
156
- id,
157
- open: true,
158
- onOpenChange: (open) => {
159
- if (!open) dismiss()
160
- },
161
- },
162
- })
163
-
164
- return {
165
- id: id,
166
- dismiss,
167
- update,
168
- }
169
- }
170
-
171
- function useToast() {
172
- const [state, setState] = React.useState<State>(memoryState)
173
-
174
- React.useEffect(() => {
175
- listeners.push(setState)
176
- return () => {
177
- const index = listeners.indexOf(setState)
178
- if (index > -1) {
179
- listeners.splice(index, 1)
180
- }
181
- }
182
- }, [state])
183
-
184
- return {
185
- ...state,
186
- toast,
187
- dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188
- }
189
- }
190
-
191
- export { useToast, toast }