@tscircuit/fake-snippets 0.0.71 → 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: