@tscircuit/fake-snippets 0.0.70 → 0.0.72

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.
@@ -1,59 +1,278 @@
1
- import { Dispatch, SetStateAction } from "react"
1
+ import { useEffect, useMemo, useState, useCallback } from "react"
2
2
  import { isValidFileName } from "@/lib/utils/isValidFileName"
3
3
  import {
4
- CodeAndPreviewState,
5
- CreateFileProps,
4
+ DEFAULT_CODE,
5
+ generateRandomPackageName,
6
+ PackageFile,
6
7
  } from "../components/package-port/CodeAndPreview"
8
+ import { Package } from "fake-snippets-api/lib/db/schema"
9
+ import {
10
+ usePackageFile,
11
+ usePackageFileById,
12
+ usePackageFiles,
13
+ } from "./use-package-files"
14
+ import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
15
+ import { usePackageFilesLoader } from "./usePackageFilesLoader"
16
+ import { useGlobalStore } from "./use-global-store"
17
+ import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
18
+ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
19
+ import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
20
+ import { useCreatePackageMutation } from "./use-create-package-mutation"
21
+ import { findTargetFile } from "@/lib/utils/findTargetFile"
22
+
23
+ export interface ICreateFileProps {
24
+ newFileName: string
25
+ onError: (error: Error) => void
26
+ }
27
+ export interface ICreateFileResult {
28
+ newFileCreated: boolean
29
+ }
30
+
31
+ export interface IDeleteFileResult {
32
+ fileDeleted: boolean
33
+ }
34
+ export interface IDeleteFileProps {
35
+ filename: string
36
+ onError: (error: Error) => void
37
+ }
38
+
39
+ export function useFileManagement({
40
+ templateCode,
41
+ currentPackage,
42
+ fileChoosen,
43
+ openNewPackageSaveDialog,
44
+ updateLastUpdated,
45
+ }: {
46
+ templateCode?: string
47
+ currentPackage?: Package
48
+ fileChoosen: string | null
49
+ openNewPackageSaveDialog: () => void
50
+ updateLastUpdated: () => void
51
+ }) {
52
+ const [localFiles, setLocalFiles] = useState<PackageFile[]>([])
53
+ const [initialFiles, setInitialFiles] = useState<PackageFile[]>([])
54
+ const [currentFile, setCurrentFile] = useState<string | null>(null)
55
+ const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
56
+ const loggedInUser = useGlobalStore((s) => s.session)
57
+ const { toast } = useToast()
58
+ const {
59
+ data: packageFilesWithContent,
60
+ isLoading: isLoadingPackageFilesWithContent,
61
+ } = usePackageFilesLoader(currentPackage)
62
+ const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
63
+ usePackageFiles(currentPackage?.latest_package_release_id)
7
64
 
8
- export function useFileManagement(
9
- state: CodeAndPreviewState,
10
- setState: Dispatch<SetStateAction<CodeAndPreviewState>>,
11
- ) {
12
- const handleCreateFile = async ({
65
+ const initialCodeContent = useMemo(() => {
66
+ return (
67
+ templateCode ??
68
+ decodeUrlHashToText(window.location.toString()) ??
69
+ DEFAULT_CODE
70
+ )
71
+ }, [templateCode, currentPackage])
72
+ const manualEditsFileContent = useMemo(() => {
73
+ return (
74
+ localFiles?.find((file) => file.path === "manual-edits.json")?.content ||
75
+ "{}"
76
+ )
77
+ }, [localFiles])
78
+
79
+ const updatePackageFilesMutation = useUpdatePackageFilesMutation({
80
+ currentPackage,
81
+ localFiles,
82
+ initialFiles,
83
+ packageFilesMeta: packageFilesMeta || [],
84
+ })
85
+ const { mutate: createRelease, isLoading: isCreatingRelease } =
86
+ useCreatePackageReleaseMutation({
87
+ onSuccess: () => {
88
+ toast({
89
+ title: "Package released",
90
+ description: "Your package has been released successfully.",
91
+ })
92
+ },
93
+ })
94
+ const createPackageMutation = useCreatePackageMutation()
95
+
96
+ useEffect(() => {
97
+ if (!currentPackage || isLoadingPackageFilesWithContent) {
98
+ setLocalFiles([
99
+ {
100
+ path: "index.tsx",
101
+ content: initialCodeContent || "",
102
+ },
103
+ ])
104
+ setInitialFiles([])
105
+ setCurrentFile("index.tsx")
106
+ return
107
+ } else {
108
+ const targetFile = findTargetFile(
109
+ packageFilesWithContent || [],
110
+ fileChoosen,
111
+ )
112
+ setLocalFiles(packageFilesWithContent || [])
113
+ setInitialFiles(packageFilesWithContent || [])
114
+ setCurrentFile(targetFile?.path || null)
115
+ }
116
+ }, [currentPackage, isLoadingPackageFilesWithContent])
117
+
118
+ const isLoading = useMemo(() => {
119
+ return (
120
+ isLoadingPackageFilesWithContent || isLoadingPackageFiles || !localFiles
121
+ )
122
+ }, [isLoadingPackageFilesWithContent, localFiles, isLoadingPackageFiles])
123
+
124
+ const fsMap = useMemo(() => {
125
+ const map = localFiles.reduce(
126
+ (acc, file) => {
127
+ acc[file.path] = file.content || ""
128
+ return acc
129
+ },
130
+ {} as Record<string, string>,
131
+ )
132
+ return map
133
+ }, [localFiles, manualEditsFileContent])
134
+
135
+ const onFileSelect = (fileName: string) => {
136
+ if (localFiles.some((file) => file.path === fileName)) {
137
+ setCurrentFile(fileName)
138
+ } else {
139
+ setCurrentFile(null)
140
+ }
141
+ }
142
+
143
+ const createFile = ({
13
144
  newFileName,
14
- setErrorMessage,
15
- onFileSelect,
16
- setNewFileName,
17
- setIsCreatingFile,
18
- }: CreateFileProps) => {
145
+ onError,
146
+ }: ICreateFileProps): ICreateFileResult => {
19
147
  newFileName = newFileName.trim()
20
148
  if (!newFileName) {
21
- setErrorMessage("File name cannot be empty")
22
- return
149
+ onError(new Error("File name cannot be empty"))
150
+ return {
151
+ newFileCreated: false,
152
+ }
23
153
  }
24
154
  if (!isValidFileName(newFileName)) {
25
- setErrorMessage(
26
- 'Invalid file name. Avoid using special characters like <>:"/\\|?*',
27
- )
28
- return
155
+ onError(new Error("Invalid file name"))
156
+ return {
157
+ newFileCreated: false,
158
+ }
29
159
  }
30
- setErrorMessage("")
31
-
32
- const fileExists = state.pkgFilesWithContent.some(
33
- (file) => file.path === newFileName,
34
- )
35
160
 
161
+ const fileExists = localFiles?.some((file) => file.path === newFileName)
36
162
  if (fileExists) {
37
- setErrorMessage("A file with this name already exists")
38
- return
163
+ onError(new Error("File already exists"))
164
+ return {
165
+ newFileCreated: false,
166
+ }
167
+ }
168
+ const updatedFiles = [
169
+ ...(localFiles || []),
170
+ { path: newFileName, content: "" },
171
+ ]
172
+ setLocalFiles(updatedFiles)
173
+ onFileSelect(newFileName)
174
+ return {
175
+ newFileCreated: true,
39
176
  }
177
+ }
40
178
 
41
- setState((prev) => {
42
- const updatedFiles = [
43
- ...prev.pkgFilesWithContent,
44
- { path: newFileName, content: "" },
45
- ]
179
+ const deleteFile = ({
180
+ filename,
181
+ onError,
182
+ }: IDeleteFileProps): IDeleteFileResult => {
183
+ const fileExists = localFiles?.some((file) => file.path === filename)
184
+ if (!fileExists) {
185
+ onError(new Error("File does not exist"))
46
186
  return {
47
- ...prev,
48
- pkgFilesWithContent: updatedFiles,
49
- } as CodeAndPreviewState
187
+ fileDeleted: false,
188
+ }
189
+ }
190
+ const updatedFiles = localFiles.filter((file) => file.path !== filename)
191
+ setLocalFiles(updatedFiles)
192
+ onFileSelect(updatedFiles[0]?.path || "")
193
+ return {
194
+ fileDeleted: true,
195
+ }
196
+ }
197
+
198
+ const savePackage = async (isPrivate: boolean) => {
199
+ if (!isLoggedIn) {
200
+ toast({
201
+ title: "Not Logged In",
202
+ description: "You must be logged in to save your package.",
203
+ })
204
+ return
205
+ }
206
+
207
+ const newPackage = await createPackageMutation.mutateAsync({
208
+ name: `${loggedInUser?.github_username}/${generateRandomPackageName()}`,
209
+ is_private: isPrivate,
50
210
  })
51
- onFileSelect(newFileName)
52
- setIsCreatingFile(false)
53
- setNewFileName("")
211
+
212
+ if (newPackage) {
213
+ createRelease(
214
+ {
215
+ package_name_with_version: `${newPackage.name}@latest`,
216
+ },
217
+ {
218
+ onSuccess: () => {
219
+ updatePackageFilesMutation.mutate({
220
+ package_name_with_version: `${newPackage.name}@latest`,
221
+ ...newPackage,
222
+ })
223
+ updateLastUpdated()
224
+ setInitialFiles([...localFiles])
225
+ },
226
+ },
227
+ )
228
+ }
229
+ updateLastUpdated()
230
+ }
231
+
232
+ const saveFiles = () => {
233
+ if (!isLoggedIn) {
234
+ toast({
235
+ title: "Not Logged In",
236
+ description: "You must be logged in to save your package.",
237
+ })
238
+ return
239
+ }
240
+ if (!currentPackage) {
241
+ openNewPackageSaveDialog()
242
+ return
243
+ }
244
+ updatePackageFilesMutation.mutate({
245
+ package_name_with_version: `${currentPackage.name}@latest`,
246
+ ...currentPackage,
247
+ })
248
+ updateLastUpdated()
249
+ setInitialFiles([...localFiles])
54
250
  }
55
251
 
252
+ const isSaving = useMemo(() => {
253
+ return (
254
+ updatePackageFilesMutation.isLoading ||
255
+ createPackageMutation.isLoading ||
256
+ isCreatingRelease
257
+ )
258
+ }, [
259
+ updatePackageFilesMutation.isLoading,
260
+ createPackageMutation.isLoading,
261
+ isCreatingRelease,
262
+ ])
263
+
56
264
  return {
57
- handleCreateFile,
265
+ fsMap,
266
+ createFile,
267
+ deleteFile,
268
+ saveFiles,
269
+ localFiles,
270
+ initialFiles,
271
+ currentFile,
272
+ setLocalFiles,
273
+ onFileSelect,
274
+ isLoading,
275
+ isSaving,
276
+ savePackage,
58
277
  }
59
278
  }
@@ -33,8 +33,8 @@ export function usePackageFilesLoader(pkg?: Package) {
33
33
  const response = await axios.post(`/package_files/get`, {
34
34
  package_file_id: file.package_file_id,
35
35
  })
36
- const content = response.data.package_file?.content_text ?? ""
37
- return content ? { path: file.file_path, content } : null
36
+ const content = response.data.package_file?.content_text
37
+ return { path: file.file_path, content: content ?? "" }
38
38
  },
39
39
  staleTime: 2,
40
40
  })) ?? [],
@@ -1,5 +1,7 @@
1
1
  import { useMutation } from "react-query"
2
2
  import type { Package } from "fake-snippets-api/lib/db/schema"
3
+ import { useAxios } from "./use-axios"
4
+ import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
3
5
 
4
6
  interface PackageFile {
5
7
  path: string
@@ -7,47 +9,49 @@ interface PackageFile {
7
9
  }
8
10
 
9
11
  interface UseUpdatePackageFilesMutationProps {
10
- pkg: Package | undefined
11
- pkgFilesWithContent: PackageFile[]
12
- initialFilesLoad: PackageFile[]
13
- pkgFiles: any
14
- axios: any
15
- toast: any
12
+ currentPackage: Package | undefined
13
+ localFiles: PackageFile[]
14
+ initialFiles: PackageFile[]
15
+ packageFilesMeta: {
16
+ created_at: string
17
+ file_path: string
18
+ package_file_id: string
19
+ package_release_id: string
20
+ }[]
16
21
  }
17
22
 
18
23
  export function useUpdatePackageFilesMutation({
19
- pkg,
20
- pkgFilesWithContent,
21
- initialFilesLoad,
22
- pkgFiles,
23
- axios,
24
- toast,
24
+ currentPackage,
25
+ localFiles,
26
+ initialFiles,
27
+ packageFilesMeta,
25
28
  }: UseUpdatePackageFilesMutationProps) {
29
+ const axios = useAxios()
30
+ const { toast } = useToast()
26
31
  return useMutation({
27
32
  mutationFn: async (
28
- newpackage: Pick<Package, "package_id" | "name"> & {
33
+ newPackage: Pick<Package, "package_id" | "name"> & {
29
34
  package_name_with_version: string
30
35
  },
31
36
  ) => {
32
- if (pkg) {
33
- newpackage = { ...pkg, ...newpackage }
37
+ if (currentPackage) {
38
+ newPackage = { ...currentPackage, ...newPackage }
34
39
  }
35
- if (!newpackage) throw new Error("No package to update")
40
+ if (!newPackage) throw new Error("No package to update")
36
41
 
37
42
  let updatedFilesCount = 0
38
43
 
39
- for (const file of pkgFilesWithContent) {
40
- const initialFile = initialFilesLoad.find((x) => x.path === file.path)
41
- if (file.content && file.content !== initialFile?.content) {
44
+ for (const file of localFiles) {
45
+ const initialFile = initialFiles.find((x) => x.path === file.path)
46
+ if (file.content !== initialFile?.content) {
42
47
  const updatePkgFilePayload = {
43
48
  package_file_id:
44
- pkgFiles.data?.find((x: any) => x.file_path === file.path)
49
+ packageFilesMeta.find((x) => x.file_path === file.path)
45
50
  ?.package_file_id ?? null,
46
51
  content_text: file.content,
47
52
  file_path: file.path,
48
- package_name_with_version: `${newpackage.name}`,
53
+ package_name_with_version: `${newPackage.name}`,
49
54
  }
50
-
51
55
  const response = await axios.post(
52
56
  "/package_files/create_or_update",
53
57
  updatePkgFilePayload,
@@ -58,10 +62,33 @@ export function useUpdatePackageFilesMutation({
58
62
  }
59
63
  }
60
64
  }
65
+
66
+ for (const initialFile of initialFiles) {
67
+ const fileStillExists = localFiles.some(
68
+ (x) => x.path === initialFile.path,
69
+ )
70
+
71
+ if (!fileStillExists) {
72
+ const fileToDelete = packageFilesMeta.find(
73
+ (x) => x.file_path === initialFile.path,
74
+ )
75
+
76
+ if (fileToDelete?.package_file_id) {
77
+ const response = await axios.post("/package_files/delete", {
78
+ package_name_with_version: `${newPackage.name}`,
79
+ file_path: initialFile.path,
80
+ })
81
+
82
+ if (response.status === 200) {
83
+ updatedFilesCount++
84
+ }
85
+ }
86
+ }
87
+ }
61
88
  return updatedFilesCount
62
89
  },
63
90
  onSuccess: (updatedFilesCount) => {
64
- if (updatedFilesCount) {
91
+ if (updatedFilesCount > 0) {
65
92
  toast({
66
93
  title: `Package's ${updatedFilesCount} files saved`,
67
94
  description: "Your changes have been saved successfully.",
@@ -69,7 +96,6 @@ export function useUpdatePackageFilesMutation({
69
96
  }
70
97
  },
71
98
  onError: (error: any) => {
72
- console.error("Error updating pkg files:", error)
73
99
  toast({
74
100
  title: "Error",
75
101
  description:
@@ -0,0 +1,52 @@
1
+ import type { QueryClient } from "react-query"
2
+
3
+ /**
4
+ * Populates React Query cache with SSR data to prevent unnecessary API calls
5
+ * on initial page load. Should be called after QueryClient creation.
6
+ */
7
+ export function populateQueryCacheWithSSRData(queryClient: QueryClient) {
8
+ if (typeof window === "undefined") return
9
+
10
+ const ssrPackage = (window as any).SSR_PACKAGE
11
+ const ssrPackageRelease = (window as any).SSR_PACKAGE_RELEASE
12
+ const ssrPackageFiles = (window as any).SSR_PACKAGE_FILES
13
+
14
+ if (ssrPackage) {
15
+ // Cache package data with all possible query keys
16
+ queryClient.setQueryData(["package", ssrPackage.package_id], ssrPackage)
17
+ queryClient.setQueryData(["package", ssrPackage.name], ssrPackage)
18
+ queryClient.setQueryData(["packages", ssrPackage.package_id], ssrPackage)
19
+ }
20
+
21
+ if (ssrPackageRelease && ssrPackage) {
22
+ // Cache package release with various query patterns
23
+ queryClient.setQueryData(
24
+ [
25
+ "packageRelease",
26
+ { package_id: ssrPackage.package_id, is_latest: true },
27
+ ],
28
+ ssrPackageRelease,
29
+ )
30
+ queryClient.setQueryData(
31
+ ["packageRelease", { package_name: ssrPackage.name, is_latest: true }],
32
+ ssrPackageRelease,
33
+ )
34
+ if (ssrPackageRelease.package_release_id) {
35
+ queryClient.setQueryData(
36
+ [
37
+ "packageRelease",
38
+ { package_release_id: ssrPackageRelease.package_release_id },
39
+ ],
40
+ ssrPackageRelease,
41
+ )
42
+ }
43
+
44
+ // Cache package files if available
45
+ if (ssrPackageFiles && ssrPackageRelease.package_release_id) {
46
+ queryClient.setQueryData(
47
+ ["packageFiles", ssrPackageRelease.package_release_id],
48
+ ssrPackageFiles,
49
+ )
50
+ }
51
+ }
52
+ }
@@ -94,10 +94,10 @@ export const DashboardPage = () => {
94
94
  </h2>
95
95
 
96
96
  <p className="text-gray-600 mb-6 text-center max-w-md text-sm sm:text-base">
97
- Log in to access your dashboard and manage your snippets.
97
+ Log in to access your dashboard and manage your packages.
98
98
  </p>
99
99
  <Button onClick={() => signIn()} variant="outline">
100
- Log in
100
+ Log In
101
101
  </Button>
102
102
  </div>
103
103
  ) : (
@@ -60,16 +60,27 @@ export function LandingPage() {
60
60
  </p>
61
61
  </div>
62
62
  <div className="flex flex-col items-center gap-2 min-[500px]:flex-row">
63
- <a href="https://docs.tscircuit.com">
64
- <Button size="lg" aria-label="Get started with TSCircuit">
63
+ <a
64
+ href="https://docs.tscircuit.com"
65
+ className="w-[70vw] min-[500px]:w-auto"
66
+ >
67
+ <Button
68
+ size="lg"
69
+ aria-label="Get started with TSCircuit"
70
+ className="w-full min-[500px]:w-auto"
71
+ >
65
72
  Get Started
66
73
  </Button>
67
74
  </a>
68
- <PrefetchPageLink href="/quickstart">
75
+ <PrefetchPageLink
76
+ href="/seveibar/usb-c-flashlight#3d"
77
+ className="w-[70vw] min-[500px]:w-auto"
78
+ >
69
79
  <Button
70
80
  size="lg"
71
81
  variant="outline"
72
82
  aria-label="Open online example of TSCircuit"
83
+ className="w-full min-[500px]:w-auto"
73
84
  >
74
85
  Open Online Example
75
86
  </Button>