@tscircuit/fake-snippets 0.0.100 → 0.0.102

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 (61) hide show
  1. package/api/generated-index.js +23 -1
  2. package/bun.lock +2 -2
  3. package/dist/bundle.js +620 -412
  4. package/dist/index.d.ts +33 -4
  5. package/dist/index.js +43 -1
  6. package/dist/schema.d.ts +94 -1
  7. package/dist/schema.js +17 -1
  8. package/fake-snippets-api/lib/db/db-client.ts +38 -1
  9. package/fake-snippets-api/lib/db/schema.ts +15 -0
  10. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +2 -0
  11. package/fake-snippets-api/routes/api/accounts/search.ts +20 -0
  12. package/fake-snippets-api/routes/api/github/installations/create_new_installation_redirect.ts +75 -0
  13. package/fake-snippets-api/routes/api/github/repos/list_available.ts +91 -0
  14. package/fake-snippets-api/routes/api/packages/update.ts +4 -0
  15. package/package.json +2 -2
  16. package/src/App.tsx +10 -1
  17. package/src/components/CmdKMenu.tsx +154 -19
  18. package/src/components/CreateReleaseDialog.tsx +124 -0
  19. package/src/components/FileSidebar.tsx +128 -23
  20. package/src/components/Header2.tsx +106 -25
  21. package/src/components/PackageBuildsPage/package-build-header.tsx +28 -16
  22. package/src/components/PageSearchComponent.tsx +2 -2
  23. package/src/components/SearchComponent.tsx +2 -2
  24. package/src/components/SuspenseRunFrame.tsx +2 -2
  25. package/src/components/TrendingPackagesCarousel.tsx +2 -2
  26. package/src/components/ViewPackagePage/components/important-files-view.tsx +18 -13
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -0
  28. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +1 -0
  29. package/src/components/dialogs/GitHubRepositorySelector.tsx +123 -0
  30. package/src/components/dialogs/create-use-dialog.tsx +8 -2
  31. package/src/components/dialogs/edit-package-details-dialog.tsx +22 -3
  32. package/src/components/dialogs/view-ts-files-dialog.tsx +178 -33
  33. package/src/components/package-port/CodeAndPreview.tsx +4 -1
  34. package/src/components/package-port/CodeEditor.tsx +42 -35
  35. package/src/components/package-port/CodeEditorHeader.tsx +26 -20
  36. package/src/components/package-port/EditorNav.tsx +94 -37
  37. package/src/components/preview/BuildsList.tsx +238 -0
  38. package/src/components/preview/ConnectedRepoDashboard.tsx +258 -0
  39. package/src/components/preview/ConnectedRepoOverview.tsx +454 -0
  40. package/src/components/preview/ConnectedRepoSettings.tsx +343 -0
  41. package/src/components/preview/ConnectedReposCards.tsx +191 -0
  42. package/src/components/preview/index.tsx +207 -0
  43. package/src/components/ui/tree-view.tsx +23 -6
  44. package/src/hooks/use-axios.ts +2 -2
  45. package/src/hooks/use-create-release-dialog.ts +160 -0
  46. package/src/hooks/use-package-details-form.ts +7 -0
  47. package/src/hooks/use-packages-base-api-url.ts +1 -1
  48. package/src/hooks/use-sign-in.ts +2 -2
  49. package/src/hooks/useFileManagement.ts +22 -2
  50. package/src/index.css +4 -0
  51. package/src/lib/utils/formatTimeAgo.ts +10 -0
  52. package/src/lib/utils/isValidFileName.ts +15 -3
  53. package/src/pages/dashboard.tsx +2 -2
  54. package/src/pages/dev-login.tsx +2 -2
  55. package/src/pages/landing.tsx +1 -1
  56. package/src/pages/latest.tsx +2 -2
  57. package/src/pages/preview-build.tsx +380 -0
  58. package/src/pages/search.tsx +2 -2
  59. package/src/pages/trending.tsx +2 -2
  60. package/src/pages/user-profile.tsx +32 -24
  61. package/src/pages/view-connected-repo.tsx +24 -0
@@ -0,0 +1,207 @@
1
+ export { ConnectedRepoOverview } from "./ConnectedRepoOverview"
2
+ export { BuildsList } from "./BuildsList"
3
+ export { ConnectedRepoSettings } from "./ConnectedRepoSettings"
4
+ export { ConnectedRepoDashboard } from "./ConnectedRepoDashboard"
5
+
6
+ export const getBuildStatus = (build: PackageBuild) => {
7
+ if (
8
+ build.build_error ||
9
+ build.transpilation_error ||
10
+ build.circuit_json_build_error
11
+ ) {
12
+ return { status: "error", label: "Failed" }
13
+ }
14
+ if (
15
+ build.build_in_progress ||
16
+ build.transpilation_in_progress ||
17
+ build.circuit_json_build_in_progress
18
+ ) {
19
+ return { status: "building", label: "Building" }
20
+ }
21
+ if (build.build_completed_at && build.transpilation_completed_at) {
22
+ return { status: "success", label: "Ready" }
23
+ }
24
+ return { status: "queued", label: "Queued" }
25
+ }
26
+
27
+ export interface PackageBuild {
28
+ package_build_id: string
29
+ package_release_id: string | null
30
+ created_at: string
31
+ transpilation_in_progress: boolean
32
+ transpilation_started_at: string | null
33
+ transpilation_completed_at: string | null
34
+ transpilation_logs: any[]
35
+ transpilation_error: string | null
36
+ circuit_json_build_in_progress: boolean
37
+ circuit_json_build_started_at: string | null
38
+ circuit_json_build_completed_at: string | null
39
+ circuit_json_build_logs: any[]
40
+ circuit_json_build_error: string | null
41
+ build_in_progress: boolean
42
+ build_started_at: string | null
43
+ build_completed_at: string | null
44
+ build_error: string | null
45
+ build_error_last_updated_at: string
46
+ preview_url: string | null
47
+ build_logs: string | null
48
+ branch_name: string | null
49
+ commit_message: string | null
50
+ commit_author: string | null
51
+ }
52
+
53
+ export const MOCK_DEPLOYMENTS: PackageBuild[] = [
54
+ {
55
+ package_build_id: "pb_1a2b3c4d",
56
+ package_release_id: "pr_5e6f7g8h",
57
+ created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
58
+ transpilation_in_progress: false,
59
+ transpilation_started_at: new Date(
60
+ Date.now() - 1000 * 60 * 35,
61
+ ).toISOString(),
62
+ transpilation_completed_at: new Date(
63
+ Date.now() - 1000 * 60 * 32,
64
+ ).toISOString(),
65
+ transpilation_logs: [],
66
+ transpilation_error: null,
67
+ circuit_json_build_in_progress: false,
68
+ circuit_json_build_started_at: new Date(
69
+ Date.now() - 1000 * 60 * 32,
70
+ ).toISOString(),
71
+ circuit_json_build_completed_at: new Date(
72
+ Date.now() - 1000 * 60 * 30,
73
+ ).toISOString(),
74
+ circuit_json_build_logs: [],
75
+ circuit_json_build_error: null,
76
+ build_in_progress: false,
77
+ build_started_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
78
+ build_completed_at: new Date(Date.now() - 1000 * 60 * 25).toISOString(),
79
+ build_error: null,
80
+ build_error_last_updated_at: new Date(
81
+ Date.now() - 1000 * 60 * 25,
82
+ ).toISOString(),
83
+ build_logs: null,
84
+ preview_url: "https://preview.tscircuit.com/pb_1a2b3c4d",
85
+ branch_name: "main",
86
+ commit_message: "Add new LED component with improved brightness control",
87
+ commit_author: "john.doe",
88
+ },
89
+ {
90
+ package_build_id: "pb_9i8j7k6l",
91
+ package_release_id: "pr_5m4n3o2p",
92
+ created_at: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
93
+ transpilation_in_progress: false,
94
+ transpilation_started_at: new Date(
95
+ Date.now() - 1000 * 60 * 60 * 2,
96
+ ).toISOString(),
97
+ transpilation_completed_at: new Date(
98
+ Date.now() - 1000 * 60 * 60 * 2 + 1000 * 60 * 3,
99
+ ).toISOString(),
100
+ transpilation_logs: [],
101
+ transpilation_error: null,
102
+ circuit_json_build_in_progress: true,
103
+ circuit_json_build_started_at: new Date(
104
+ Date.now() - 1000 * 60 * 5,
105
+ ).toISOString(),
106
+ circuit_json_build_completed_at: null,
107
+ circuit_json_build_logs: [],
108
+ circuit_json_build_error: null,
109
+ build_in_progress: false,
110
+ build_started_at: null,
111
+ build_completed_at: null,
112
+ build_error: null,
113
+ build_error_last_updated_at: new Date(
114
+ Date.now() - 1000 * 60 * 60 * 2,
115
+ ).toISOString(),
116
+ build_logs: null,
117
+ preview_url: null,
118
+ branch_name: "feature/resistor-update",
119
+ commit_message: "Update resistor component with new tolerance values",
120
+ commit_author: "jane.smith",
121
+ },
122
+ {
123
+ package_build_id: "pb_1q2w3e4r",
124
+ package_release_id: "pr_5t6y7u8i",
125
+ created_at: new Date(Date.now() - 1000 * 60 * 60 * 6).toISOString(),
126
+ transpilation_in_progress: false,
127
+ transpilation_started_at: new Date(
128
+ Date.now() - 1000 * 60 * 60 * 6,
129
+ ).toISOString(),
130
+ transpilation_completed_at: new Date(
131
+ Date.now() - 1000 * 60 * 60 * 6 + 1000 * 60 * 2,
132
+ ).toISOString(),
133
+ transpilation_logs: [],
134
+ transpilation_error:
135
+ "TypeScript compilation failed: Cannot find module 'missing-dependency'",
136
+ circuit_json_build_in_progress: false,
137
+ circuit_json_build_started_at: null,
138
+ circuit_json_build_completed_at: null,
139
+ circuit_json_build_logs: [],
140
+ circuit_json_build_error: null,
141
+ build_in_progress: false,
142
+ build_started_at: null,
143
+ build_completed_at: null,
144
+ build_error: null,
145
+ build_error_last_updated_at: new Date(
146
+ Date.now() - 1000 * 60 * 60 * 6,
147
+ ).toISOString(),
148
+ build_logs: null,
149
+ preview_url: null,
150
+ branch_name: "hotfix/critical-bug",
151
+ commit_message: "Fix critical issue with capacitor placement",
152
+ commit_author: "alex.wilson",
153
+ },
154
+ {
155
+ package_build_id: "pb_9o8i7u6y",
156
+ package_release_id: "pr_5t4r3e2w",
157
+ created_at: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
158
+ transpilation_in_progress: false,
159
+ transpilation_started_at: new Date(
160
+ Date.now() - 1000 * 60 * 60 * 24,
161
+ ).toISOString(),
162
+ transpilation_completed_at: new Date(
163
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 4,
164
+ ).toISOString(),
165
+ transpilation_logs: [],
166
+ transpilation_error: null,
167
+ circuit_json_build_in_progress: false,
168
+ circuit_json_build_started_at: new Date(
169
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 4,
170
+ ).toISOString(),
171
+ circuit_json_build_completed_at: new Date(
172
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 8,
173
+ ).toISOString(),
174
+ circuit_json_build_logs: [],
175
+ circuit_json_build_error: null,
176
+ build_in_progress: false,
177
+ build_started_at: new Date(
178
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 8,
179
+ ).toISOString(),
180
+ build_completed_at: new Date(
181
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 12,
182
+ ).toISOString(),
183
+ build_error: null,
184
+ build_error_last_updated_at: new Date(
185
+ Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 12,
186
+ ).toISOString(),
187
+ build_logs: null,
188
+ preview_url: "https://preview.tscircuit.com/pb_9o8i7u6y",
189
+ branch_name: "main",
190
+ commit_message: "Initial project setup with basic components",
191
+ commit_author: "sarah.johnson",
192
+ },
193
+ ]
194
+
195
+ import { Clock, CheckCircle, AlertCircle, Loader2 } from "lucide-react"
196
+ export const StatusIcon = ({ status }: { status: string }) => {
197
+ switch (status) {
198
+ case "success":
199
+ return <CheckCircle className="w-4 h-4 text-green-500" />
200
+ case "error":
201
+ return <AlertCircle className="w-4 h-4 text-red-500" />
202
+ case "building":
203
+ return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />
204
+ default:
205
+ return <Clock className="w-4 h-4 text-gray-500" />
206
+ }
207
+ }
@@ -40,6 +40,8 @@ type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
40
40
  expandAll?: boolean
41
41
  defaultNodeIcon?: any
42
42
  defaultLeafIcon?: any
43
+ selectedItemId: string
44
+ setSelectedItemId: (id: string | undefined) => void
43
45
  onDocumentDrag?: (sourceItem: TreeDataItem, targetItem: TreeDataItem) => void
44
46
  }
45
47
 
@@ -54,14 +56,12 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
54
56
  defaultNodeIcon,
55
57
  className,
56
58
  onDocumentDrag,
59
+ selectedItemId,
60
+ setSelectedItemId,
57
61
  ...props
58
62
  },
59
63
  ref,
60
64
  ) => {
61
- const [selectedItemId, setSelectedItemId] = React.useState<
62
- string | undefined
63
- >(initialSelectedItemId)
64
-
65
65
  React.useEffect(() => {
66
66
  setSelectedItemId(initialSelectedItemId)
67
67
  }, [initialSelectedItemId])
@@ -129,6 +129,7 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
129
129
  <TreeItem
130
130
  data={data}
131
131
  ref={ref}
132
+ setSelectedItemId={setSelectedItemId}
132
133
  selectedItemId={selectedItemId}
133
134
  handleSelectChange={handleSelectChange}
134
135
  expandedItemIds={expandedItemIds}
@@ -159,6 +160,7 @@ type TreeItemProps = TreeProps & {
159
160
  defaultLeafIcon?: any
160
161
  handleDragStart?: (item: TreeDataItem) => void
161
162
  handleDrop?: (item: TreeDataItem) => void
163
+ setSelectedItemId: (id: string | undefined) => void
162
164
  draggedItem: TreeDataItem | null
163
165
  }
164
166
 
@@ -169,6 +171,7 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
169
171
  data,
170
172
  selectedItemId,
171
173
  handleSelectChange,
174
+ setSelectedItemId,
172
175
  expandedItemIds,
173
176
  defaultNodeIcon,
174
177
  defaultLeafIcon,
@@ -182,14 +185,25 @@ const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
182
185
  if (!(data instanceof Array)) {
183
186
  data = [data]
184
187
  }
188
+
189
+ const sortedData = [...data].sort((a, b) => {
190
+ const aIsFolder = !!a.children
191
+ const bIsFolder = !!b.children
192
+
193
+ if (aIsFolder && !bIsFolder) return -1
194
+ if (!aIsFolder && bIsFolder) return 1
195
+ return 0
196
+ })
197
+
185
198
  return (
186
199
  <div ref={ref} role="tree" className={className} {...props}>
187
200
  <ul>
188
- {data.map((item) => (
201
+ {sortedData.map((item) => (
189
202
  <li key={item.id}>
190
203
  {item.children ? (
191
204
  <TreeNode
192
205
  item={item}
206
+ setSelectedItemId={setSelectedItemId}
193
207
  selectedItemId={selectedItemId}
194
208
  expandedItemIds={expandedItemIds}
195
209
  handleSelectChange={handleSelectChange}
@@ -227,13 +241,15 @@ const TreeNode = ({
227
241
  defaultNodeIcon,
228
242
  defaultLeafIcon,
229
243
  handleDragStart,
244
+ setSelectedItemId,
230
245
  handleDrop,
231
246
  draggedItem,
232
247
  }: {
233
248
  item: TreeDataItem
234
249
  handleSelectChange: (item: TreeDataItem | undefined) => void
235
250
  expandedItemIds: string[]
236
- selectedItemId?: string
251
+ selectedItemId: string
252
+ setSelectedItemId: (id: string | undefined) => void
237
253
  defaultNodeIcon?: any
238
254
  defaultLeafIcon?: any
239
255
  handleDragStart?: (item: TreeDataItem) => void
@@ -309,6 +325,7 @@ const TreeNode = ({
309
325
  <TreeItem
310
326
  data={item.children ? item.children : item}
311
327
  selectedItemId={selectedItemId}
328
+ setSelectedItemId={setSelectedItemId}
312
329
  handleSelectChange={handleSelectChange}
313
330
  expandedItemIds={expandedItemIds}
314
331
  defaultLeafIcon={defaultLeafIcon}
@@ -1,10 +1,10 @@
1
1
  import axios from "redaxios"
2
2
  import { useMemo } from "react"
3
3
  import { useGlobalStore } from "./use-global-store"
4
- import { usePackagesBaseApiUrl } from "./use-packages-base-api-url"
4
+ import { useApiBaseUrl } from "./use-packages-base-api-url"
5
5
 
6
6
  export const useAxios = () => {
7
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
7
+ const snippetsBaseApiUrl = useApiBaseUrl()
8
8
  const session = useGlobalStore((s) => s.session)
9
9
  return useMemo(() => {
10
10
  const instance = axios.create({
@@ -0,0 +1,160 @@
1
+ import { useState } from "react"
2
+ import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
3
+ import { useGlobalStore } from "./use-global-store"
4
+ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
5
+ import type { PackageFile } from "@/types/package"
6
+ import type { Package } from "fake-snippets-api/lib/db/schema"
7
+ import { useToast } from "./use-toast"
8
+
9
+ interface UseCreateReleaseDialogProps {
10
+ packageId?: string
11
+ packageName?: string
12
+ currentVersion?: string
13
+ onSuccess?: (release: any) => void
14
+ files?: PackageFile[]
15
+ currentPackage?: Package
16
+ packageFilesMeta?: {
17
+ created_at: string
18
+ file_path: string
19
+ package_file_id: string
20
+ package_release_id: string
21
+ }[]
22
+ }
23
+
24
+ export const useCreateReleaseDialog = ({
25
+ packageId,
26
+ packageName,
27
+ currentVersion,
28
+ onSuccess,
29
+ files = [],
30
+ currentPackage,
31
+ packageFilesMeta = [],
32
+ }: UseCreateReleaseDialogProps) => {
33
+ const { toast } = useToast()
34
+ const [isOpen, setIsOpen] = useState(false)
35
+ const [version, setVersion] = useState("")
36
+ const [isLoading, setIsLoading] = useState(false)
37
+ const [error, setError] = useState<string | null>(null)
38
+
39
+ const suggestedNextVersion = currentVersion
40
+ ? (() => {
41
+ const parts = currentVersion.split(".")
42
+ if (parts.length === 3) {
43
+ const [major, minor, patch] = parts.map(Number)
44
+ if (!isNaN(major) && !isNaN(minor) && !isNaN(patch)) {
45
+ return `${major}.${minor}.${patch + 1}`
46
+ }
47
+ }
48
+ return undefined
49
+ })()
50
+ : undefined
51
+
52
+ const { mutateAsync: createRelease } = useCreatePackageReleaseMutation({
53
+ onSuccess: () => {
54
+ toast({
55
+ title: "Package release created",
56
+ description: "Your package release has been created successfully.",
57
+ })
58
+ },
59
+ })
60
+ const session = useGlobalStore((s) => s.session)
61
+
62
+ const updatePackageFilesMutation = useUpdatePackageFilesMutation({
63
+ currentPackage,
64
+ localFiles: files,
65
+ initialFiles: [],
66
+ packageFilesMeta,
67
+ })
68
+
69
+ const open = () => {
70
+ setIsOpen(true)
71
+ setError(null)
72
+ // Auto-fill with suggested next version
73
+ const nextVersion = suggestedNextVersion || "0.0.1"
74
+ setVersion(nextVersion)
75
+ }
76
+
77
+ const close = () => {
78
+ setIsOpen(false)
79
+ setError(null)
80
+ setVersion("")
81
+ }
82
+
83
+ const handleCreateRelease = async () => {
84
+ if (!version.trim()) {
85
+ setError("Version is required")
86
+ return
87
+ }
88
+
89
+ if (!packageId && !packageName) {
90
+ setError("Package information is missing")
91
+ return
92
+ }
93
+
94
+ if (!session) {
95
+ setError("You must be logged in to create a release")
96
+ return
97
+ }
98
+
99
+ setIsLoading(true)
100
+ setError(null)
101
+
102
+ try {
103
+ const releaseData: any = {
104
+ version: version.trim(),
105
+ is_latest: true,
106
+ }
107
+
108
+ if (packageId) {
109
+ releaseData.package_id = packageId
110
+ } else if (packageName) {
111
+ releaseData.package_name = packageName
112
+ }
113
+
114
+ const result = await createRelease(releaseData)
115
+
116
+ if (files.length > 0 && currentPackage) {
117
+ try {
118
+ await updatePackageFilesMutation.mutateAsync({
119
+ package_name_with_version: `${currentPackage.name}@${version.trim()}`,
120
+ ...currentPackage,
121
+ })
122
+ } catch (fileError: any) {
123
+ console.error("Error uploading files:", fileError)
124
+ toast({
125
+ title: "Error",
126
+ description: "Failed to upload some files to the release",
127
+ })
128
+ }
129
+ }
130
+
131
+ onSuccess?.(result)
132
+ close()
133
+ } catch (err: any) {
134
+ const errorMessage =
135
+ err?.response?.data?.error?.message ||
136
+ err?.message ||
137
+ "Failed to create release"
138
+ setError(errorMessage)
139
+ toast({
140
+ title: "Error",
141
+ description: errorMessage,
142
+ })
143
+ } finally {
144
+ setIsLoading(false)
145
+ }
146
+ }
147
+
148
+ return {
149
+ isOpen,
150
+ openDialog: open,
151
+ closeDialog: close,
152
+ version,
153
+ setVersion,
154
+ currentVersion,
155
+ suggestedNextVersion,
156
+ isLoading: isLoading || updatePackageFilesMutation.isLoading,
157
+ error,
158
+ handleCreateRelease,
159
+ }
160
+ }
@@ -16,6 +16,7 @@ interface PackageDetailsForm {
16
16
  license: string | null
17
17
  visibility: string
18
18
  defaultView: string
19
+ githubRepoFullName: string | null
19
20
  unscopedPackageName: string
20
21
  }
21
22
 
@@ -26,6 +27,7 @@ interface UsePackageDetailsFormProps {
26
27
  initialVisibility: string
27
28
  initialDefaultView: string
28
29
  initialUnscopedPackageName: string
30
+ initialGithubRepoFullName: string | null
29
31
  isDialogOpen: boolean
30
32
  }
31
33
 
@@ -36,6 +38,7 @@ export const usePackageDetailsForm = ({
36
38
  initialVisibility,
37
39
  initialDefaultView,
38
40
  initialUnscopedPackageName,
41
+ initialGithubRepoFullName,
39
42
  isDialogOpen,
40
43
  }: UsePackageDetailsFormProps) => {
41
44
  const [formData, setFormData] = useState<PackageDetailsForm>({
@@ -44,6 +47,7 @@ export const usePackageDetailsForm = ({
44
47
  license: initialLicense || null,
45
48
  visibility: initialVisibility,
46
49
  defaultView: initialDefaultView,
50
+ githubRepoFullName: initialGithubRepoFullName,
47
51
  unscopedPackageName: initialUnscopedPackageName,
48
52
  })
49
53
  const [websiteError, setWebsiteError] = useState<string | null>(null)
@@ -55,6 +59,7 @@ export const usePackageDetailsForm = ({
55
59
  website: initialWebsite,
56
60
  license: initialLicense || null,
57
61
  visibility: initialVisibility,
62
+ githubRepoFullName: initialGithubRepoFullName,
58
63
  defaultView: initialDefaultView,
59
64
  unscopedPackageName: initialUnscopedPackageName,
60
65
  })
@@ -100,6 +105,7 @@ export const usePackageDetailsForm = ({
100
105
  formData.license !== initialLicense ||
101
106
  formData.visibility !== initialVisibility ||
102
107
  formData.defaultView !== initialDefaultView ||
108
+ formData.githubRepoFullName !== initialGithubRepoFullName ||
103
109
  formData.unscopedPackageName !== initialUnscopedPackageName,
104
110
  [
105
111
  formData,
@@ -108,6 +114,7 @@ export const usePackageDetailsForm = ({
108
114
  initialLicense,
109
115
  initialVisibility,
110
116
  initialDefaultView,
117
+ initialGithubRepoFullName,
111
118
  initialUnscopedPackageName,
112
119
  ],
113
120
  )
@@ -1,3 +1,3 @@
1
- export const usePackagesBaseApiUrl = () => {
1
+ export const useApiBaseUrl = () => {
2
2
  return import.meta.env.VITE_SNIPPETS_API_URL ?? "/api"
3
3
  }
@@ -1,9 +1,9 @@
1
1
  import { useGlobalStore } from "./use-global-store"
2
2
  import { useIsUsingFakeApi } from "./use-is-using-fake-api"
3
- import { usePackagesBaseApiUrl } from "./use-packages-base-api-url"
3
+ import { useApiBaseUrl } from "./use-packages-base-api-url"
4
4
 
5
5
  export const useSignIn = () => {
6
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
6
+ const snippetsBaseApiUrl = useApiBaseUrl()
7
7
  const isUsingFakeApi = useIsUsingFakeApi()
8
8
  const setSession = useGlobalStore((s) => s.setSession)
9
9
  return () => {
@@ -196,34 +196,53 @@ export function useFileManagement({
196
196
  openFile = true,
197
197
  }: ICreateFileProps): ICreateFileResult => {
198
198
  newFileName = newFileName.trim()
199
+
199
200
  if (!newFileName) {
200
201
  onError(new Error("File name cannot be empty"))
201
202
  return {
202
203
  newFileCreated: false,
203
204
  }
204
205
  }
206
+
205
207
  if (!isValidFileName(newFileName)) {
206
- onError(new Error("Invalid file name"))
208
+ onError(
209
+ new Error(
210
+ "Invalid file name. Avoid special characters and relative paths (. or ..)",
211
+ ),
212
+ )
207
213
  return {
208
214
  newFileCreated: false,
209
215
  }
210
216
  }
211
217
 
218
+ // Check if file already exists
212
219
  const fileExists = localFiles?.some((file) => file.path === newFileName)
213
220
  if (fileExists) {
214
- onError(new Error("File already exists"))
221
+ onError(new Error(`File '${newFileName}' already exists`))
215
222
  return {
216
223
  newFileCreated: false,
217
224
  }
218
225
  }
226
+
227
+ // Ensure file name is not empty after path construction
228
+ const fileName = newFileName.split("/").pop() || ""
229
+ if (!fileName.trim()) {
230
+ onError(new Error("File name cannot be empty"))
231
+ return {
232
+ newFileCreated: false,
233
+ }
234
+ }
235
+
219
236
  const updatedFiles = [
220
237
  ...(localFiles || []),
221
238
  { path: newFileName, content: content || "" },
222
239
  ]
223
240
  setLocalFiles(updatedFiles)
241
+
224
242
  if (openFile) {
225
243
  setCurrentFile(newFileName)
226
244
  }
245
+
227
246
  return {
228
247
  newFileCreated: true,
229
248
  }
@@ -428,5 +447,6 @@ export function useFileManagement({
428
447
  isLoading,
429
448
  isSaving,
430
449
  savePackage,
450
+ packageFilesMeta,
431
451
  }
432
452
  }
package/src/index.css CHANGED
@@ -78,3 +78,7 @@
78
78
  max-width: none !important;
79
79
  width: 100% !important;
80
80
  }
81
+
82
+ body {
83
+ overflow-x: hidden;
84
+ }
@@ -0,0 +1,10 @@
1
+ export const formatTimeAgo = (dateString: string) => {
2
+ const date = new Date(dateString)
3
+ const now = new Date()
4
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
5
+
6
+ if (diffInSeconds < 60) return "just now"
7
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`
8
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`
9
+ return `${Math.floor(diffInSeconds / 86400)}d ago`
10
+ }
@@ -1,5 +1,17 @@
1
1
  export const isValidFileName = (name: string) => {
2
- // Basic checks for file naming conventions
3
- const invalidChars = /[<>:"/\\|?*]/
4
- return name.length > 0 && !invalidChars.test(name)
2
+ const invalidChars = /[<>:"\\|?*]/
3
+
4
+ if (name.length === 0) return false
5
+ if (invalidChars.test(name)) return false
6
+
7
+ const pathParts = name.split("/")
8
+
9
+ for (const part of pathParts) {
10
+ if (part === "") continue
11
+ if (part === "." || part === "..") return false
12
+ if (part.startsWith(" ") || part.endsWith(" ")) return false
13
+ if (part.includes("\\")) return false
14
+ }
15
+
16
+ return true
5
17
  }
@@ -12,7 +12,7 @@ import { PrefetchPageLink } from "@/components/PrefetchPageLink"
12
12
  import { PackagesList } from "@/components/PackagesList"
13
13
  import { Helmet } from "react-helmet-async"
14
14
  import { useSignIn } from "@/hooks/use-sign-in"
15
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
15
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
16
16
  import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
17
17
  import { PackageCardSkeleton } from "@/components/PackageCardSkeleton"
18
18
  import { PackageCard } from "@/components/PackageCard"
@@ -85,7 +85,7 @@ export const DashboardPage = () => {
85
85
  },
86
86
  )
87
87
 
88
- const baseUrl = usePackagesBaseApiUrl()
88
+ const baseUrl = useApiBaseUrl()
89
89
 
90
90
  const handleDeleteClick = (e: React.MouseEvent, pkg: Package) => {
91
91
  e.preventDefault() // Prevent navigation
@@ -1,10 +1,10 @@
1
1
  import { useGlobalStore } from "@/hooks/use-global-store"
2
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
2
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
3
3
  import { useState } from "react"
4
4
  import { useLocation } from "wouter"
5
5
 
6
6
  export const DevLoginPage = () => {
7
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
7
+ const snippetsBaseApiUrl = useApiBaseUrl()
8
8
  const [username, setUsername] = useState("")
9
9
  const setSession = useGlobalStore((s) => s.setSession)
10
10
  const [, setLocation] = useLocation()