@tscircuit/fake-snippets 0.0.44 → 0.0.45

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 (32) hide show
  1. package/bun.lock +43 -11
  2. package/dist/bundle.js +405 -335
  3. package/fake-snippets-api/routes/api/_fake/received_quotes.ts +66 -0
  4. package/fake-snippets-api/routes/api/package_releases/update.ts +25 -18
  5. package/package.json +2 -2
  6. package/src/components/CodeAndPreview.tsx +0 -1
  7. package/src/components/CodeEditor.tsx +0 -1
  8. package/src/components/CodeEditorHeader.tsx +0 -25
  9. package/src/components/EditorNav.tsx +10 -8
  10. package/src/components/ErrorOutline.tsx +35 -0
  11. package/src/components/FileSidebar.tsx +46 -16
  12. package/src/components/NotFound.tsx +37 -0
  13. package/src/components/PreviewContent.tsx +0 -6
  14. package/src/components/TrendingSnippetCarousel.tsx +1 -1
  15. package/src/components/ViewPackagePage/components/package-header.tsx +24 -3
  16. package/src/components/ViewPackagePage/utils/is-hidden-file.ts +0 -1
  17. package/src/components/dialogs/package-visibility-settings-dialog.tsx +10 -1
  18. package/src/components/dialogs/view-ts-files-dialog.tsx +0 -6
  19. package/src/components/package-port/CodeAndPreview.tsx +10 -8
  20. package/src/components/package-port/CodeEditor.tsx +26 -38
  21. package/src/components/package-port/CodeEditorHeader.tsx +117 -39
  22. package/src/components/package-port/EditorNav.tsx +10 -8
  23. package/src/components/ui/tree-view.tsx +5 -1
  24. package/src/{prettier.ts → lib/types.ts} +3 -1
  25. package/src/lib/utils/findTargetFile.ts +62 -0
  26. package/src/lib/utils/load-prettier.ts +3 -0
  27. package/src/pages/404.tsx +2 -33
  28. package/src/pages/package-editor.tsx +14 -3
  29. package/src/pages/user-profile.tsx +66 -27
  30. package/src/components/FootprintDialog.tsx +0 -339
  31. package/src/components/ParametersEditor.tsx +0 -140
  32. package/src/lib/utils/parseFootprintParams.ts +0 -52
@@ -26,19 +26,15 @@ import { EditorView } from "codemirror"
26
26
  import { useEffect, useMemo, useRef, useState } from "react"
27
27
  import ts from "typescript"
28
28
  import CodeEditorHeader from "@/components/package-port/CodeEditorHeader"
29
- // import { copilotPlugin, Language } from "@valtown/codemirror-codeium"
30
29
  import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
31
30
  import FileSidebar from "../FileSidebar"
32
-
31
+ import { findTargetFile } from "@/lib/utils/findTargetFile"
32
+ import type { PackageFile } from "./CodeAndPreview"
33
33
  const defaultImports = `
34
34
  import React from "@types/react/jsx-runtime"
35
35
  import { Circuit, createUseComponent } from "@tscircuit/core"
36
36
  import type { CommonLayoutProps } from "@tscircuit/props"
37
37
  `
38
- export interface FileContent {
39
- path: string
40
- content: string
41
- }
42
38
 
43
39
  export const CodeEditor = ({
44
40
  onCodeChange,
@@ -48,12 +44,14 @@ export const CodeEditor = ({
48
44
  isStreaming = false,
49
45
  showImportAndFormatButtons = true,
50
46
  onFileContentChanged,
47
+ pkgFilesLoaded,
51
48
  }: {
52
49
  onCodeChange: (code: string, filename?: string) => void
53
50
  onDtsChange?: (dts: string) => void
54
- files: FileContent[]
51
+ files: PackageFile[]
55
52
  readOnly?: boolean
56
53
  isStreaming?: boolean
54
+ pkgFilesLoaded?: boolean
57
55
  showImportAndFormatButtons?: boolean
58
56
  onFileContentChanged?: (path: string, content: string) => void
59
57
  }) => {
@@ -62,7 +60,6 @@ export const CodeEditor = ({
62
60
  const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
63
61
  const apiUrl = useSnippetsBaseApiUrl()
64
62
  const codeCompletionApi = useCodeCompletionApi()
65
-
66
63
  const [cursorPosition, setCursorPosition] = useState<number | null>(null)
67
64
  const [code, setCode] = useState(files[0]?.content || "")
68
65
  const [currentFile, setCurrentFile] = useState<string>("")
@@ -70,35 +67,17 @@ export const CodeEditor = ({
70
67
  // Get URL search params for file_path
71
68
  const urlParams = new URLSearchParams(window.location.search)
72
69
  const filePathFromUrl = urlParams.get("file_path")
73
-
74
70
  // Set current file on component mount
75
71
  useEffect(() => {
76
- if (files.length > 0 && currentFile === "") {
77
- // Priority 1: Use file_path from URL if it exists in files
78
- if (
79
- filePathFromUrl &&
80
- files.some((file) => file.path === filePathFromUrl)
81
- ) {
82
- setCurrentFile(filePathFromUrl)
83
- }
84
- // Priority 2: Use index.tsx if it exists in files
85
- else if (files.some((file) => file.path === "index.tsx")) {
86
- setCurrentFile("index.tsx")
87
- }
88
- // Priority 3: Use the first file with .tsx extension
89
- else {
90
- const tsxFile = files.find((file) => file.path.endsWith(".tsx"))
91
- if (tsxFile) {
92
- setCurrentFile(tsxFile.path)
93
- }
94
- // Fallback: Use the first file in the array
95
- else if (files[0]) {
96
- setCurrentFile(files[0].path)
97
- }
98
- }
99
- return
72
+ if (files.length === 0 || !pkgFilesLoaded || currentFile) return
73
+
74
+ const targetFile = findTargetFile(files, filePathFromUrl)
75
+
76
+ if (targetFile) {
77
+ setCurrentFile(targetFile.path)
78
+ setCode(targetFile.content)
100
79
  }
101
- }, [files])
80
+ }, [filePathFromUrl, pkgFilesLoaded])
102
81
 
103
82
  const fileMap = useMemo(() => {
104
83
  const map: Record<string, string> = {}
@@ -448,6 +427,12 @@ export const CodeEditor = ({
448
427
 
449
428
  const handleFileChange = (path: string) => {
450
429
  setCurrentFile(path)
430
+ try {
431
+ // Set url query to file path
432
+ const urlParams = new URLSearchParams(window.location.search)
433
+ urlParams.set("file_path", path)
434
+ window.history.replaceState(null, "", `?${urlParams.toString()}`)
435
+ } catch {}
451
436
  }
452
437
 
453
438
  const updateFileContent = (path: string, newContent: string) => {
@@ -478,7 +463,7 @@ export const CodeEditor = ({
478
463
  }
479
464
  const [sidebarOpen, setSidebarOpen] = useState(false)
480
465
  return (
481
- <div className="flex h-full">
466
+ <div className="flex h-full w-full">
482
467
  <FileSidebar
483
468
  files={Object.fromEntries(files.map((f) => [f.path, f.content]))}
484
469
  currentFile={currentFile}
@@ -487,7 +472,7 @@ export const CodeEditor = ({
487
472
  }
488
473
  onFileSelect={handleFileChange}
489
474
  />
490
- <div className="flex flex-col flex-1">
475
+ <div className="flex flex-col flex-1 w-full min-w-0">
491
476
  {showImportAndFormatButtons && (
492
477
  <CodeEditorHeader
493
478
  fileSidebarState={
@@ -500,10 +485,13 @@ export const CodeEditor = ({
500
485
  updateFileContent={(...args) => {
501
486
  return updateFileContent(...args)
502
487
  }}
503
- cursorPosition={cursorPosition}
488
+ handleFileChange={handleFileChange}
504
489
  />
505
490
  )}
506
- <div ref={editorRef} className="flex-1 overflow-auto max-w-[100%]" />
491
+ <div
492
+ ref={editorRef}
493
+ className="flex-1 overflow-auto [&_.cm-editor]:h-full"
494
+ />
507
495
  </div>
508
496
  </div>
509
497
  )
@@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button"
3
3
  import { handleManualEditsImport } from "@/lib/handleManualEditsImport"
4
4
  import { useImportSnippetDialog } from "@/components/dialogs/import-snippet-dialog"
5
5
  import { useToast } from "@/hooks/use-toast"
6
- import { FootprintDialog } from "@/components/FootprintDialog"
7
6
  import {
8
7
  DropdownMenu,
9
8
  DropdownMenuContent,
@@ -12,36 +11,70 @@ import {
12
11
  } from "@/components/ui/dropdown-menu"
13
12
  import { AlertTriangle, PanelRightClose } from "lucide-react"
14
13
  import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
15
-
14
+ import {
15
+ Select,
16
+ SelectContent,
17
+ SelectItem,
18
+ SelectTrigger,
19
+ SelectValue,
20
+ } from "../ui/select"
21
+ import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
16
22
  export type FileName = string
17
23
 
18
24
  interface CodeEditorHeaderProps {
19
25
  currentFile: FileName
20
26
  files: Record<FileName, string>
21
27
  updateFileContent: (filename: FileName, content: string) => void
22
- cursorPosition: number | null
23
28
  fileSidebarState: ReturnType<typeof useState<boolean>>
29
+ handleFileChange: (filename: FileName) => void
24
30
  }
25
31
 
26
32
  export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
27
33
  currentFile,
28
34
  files,
29
35
  updateFileContent,
30
- cursorPosition,
31
36
  fileSidebarState,
37
+ handleFileChange,
32
38
  }) => {
33
39
  const { Dialog: ImportSnippetDialog, openDialog: openImportDialog } =
34
40
  useImportSnippetDialog()
35
- const [footprintDialogOpen, setFootprintDialogOpen] = useState(false)
36
41
  const { toast } = useToast()
37
42
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
43
+
38
44
  const handleFormatFile = useCallback(() => {
39
45
  if (!window.prettier || !window.prettierPlugins) return
40
-
41
46
  try {
42
47
  const currentContent = files[currentFile]
48
+ let fileExtension = currentFile.split(".").pop()?.toLowerCase()
49
+ if (currentContent.trim().length === 0) {
50
+ toast({
51
+ title: "Empty file",
52
+ description: "Cannot format an empty file.",
53
+ })
54
+ return
55
+ }
56
+ if (!fileExtension) {
57
+ toast({
58
+ title: "Cannot determine file type",
59
+ description: "Unable to format file without an extension.",
60
+ })
61
+ return
62
+ }
63
+
64
+ if (["readme"].includes(currentFile.toLowerCase())) {
65
+ fileExtension = "md"
66
+ }
43
67
 
44
- if (currentFile.endsWith(".json")) {
68
+ if (fileExtension === currentFile.toLowerCase()) {
69
+ toast({
70
+ title: "Cannot determine file type",
71
+ description: "Unable to format file without an extension.",
72
+ })
73
+ return
74
+ }
75
+
76
+ // Handle JSON formatting separately
77
+ if (fileExtension === "json") {
45
78
  try {
46
79
  const jsonObj = JSON.parse(currentContent)
47
80
  const formattedJson = JSON.stringify(jsonObj, null, 2)
@@ -52,28 +85,47 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
52
85
  description: "Failed to format JSON: invalid syntax.",
53
86
  variant: "destructive",
54
87
  })
55
- return
56
88
  }
57
89
  return
58
90
  }
59
91
 
92
+ const parserMap: Record<string, string> = {
93
+ js: "babel",
94
+ jsx: "babel",
95
+ ts: "typescript",
96
+ tsx: "typescript",
97
+ md: "markdown",
98
+ markdown: "markdown",
99
+ }
100
+
101
+ const parser = parserMap[fileExtension] || "typescript"
60
102
  const formattedCode = window.prettier.format(currentContent, {
61
103
  semi: false,
62
- parser: "typescript",
104
+ parser: parser,
63
105
  plugins: window.prettierPlugins,
64
106
  })
65
107
 
66
108
  updateFileContent(currentFile, formattedCode)
67
109
  } catch (error) {
68
110
  console.error("Formatting error:", error)
69
- toast({
70
- title: "Formatting error",
71
- description:
72
- error instanceof Error
73
- ? error.message
74
- : "Failed to format the code. Please check for syntax errors.",
75
- variant: "destructive",
76
- })
111
+ if (
112
+ error instanceof Error &&
113
+ error.message.includes("No parser could be inferred")
114
+ ) {
115
+ toast({
116
+ title: "Unsupported File Type",
117
+ description: `Formatting not supported for .${currentFile.split(".").pop()?.toLowerCase()} files. Tried default parser.`,
118
+ })
119
+ } else {
120
+ toast({
121
+ title: "Formatting error",
122
+ description:
123
+ error instanceof Error
124
+ ? error.message
125
+ : "Failed to format the code. Please check for syntax errors.",
126
+ variant: "destructive",
127
+ })
128
+ }
77
129
  }
78
130
  }, [currentFile, files, toast, updateFileContent])
79
131
 
@@ -81,13 +133,59 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
81
133
  <>
82
134
  <div className="flex items-center gap-2 px-2 border-b border-gray-200">
83
135
  <button
84
- className={`text-black/60 scale-90 transition-opacity duration-200 ${sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
136
+ className={`text-gray-400 scale-90 transition-opacity duration-200 ${
137
+ sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"
138
+ }`}
85
139
  onClick={() => setSidebarOpen(true)}
86
140
  >
87
141
  <PanelRightClose />
88
142
  </button>
143
+ <div>
144
+ <Select value={currentFile} onValueChange={handleFileChange}>
145
+ <SelectTrigger className="h-7 px-3 bg-white">
146
+ <SelectValue placeholder="Select file" />
147
+ </SelectTrigger>
148
+ <SelectContent>
149
+ {Object.keys(files)
150
+ .filter(
151
+ (filename) =>
152
+ !isHiddenFile(
153
+ filename.startsWith("/") ? filename.slice(1) : filename,
154
+ ),
155
+ )
156
+ .map((filename) => (
157
+ <SelectItem className="py-1" key={filename} value={filename}>
158
+ <span
159
+ className={`text-xs pr-1 block truncate ${
160
+ sidebarOpen ? "max-w-[5rem]" : "max-w-[10rem]"
161
+ }`}
162
+ >
163
+ {filename}
164
+ </span>
165
+ </SelectItem>
166
+ ))}
167
+ {currentFile &&
168
+ Object.keys(files).includes(currentFile) &&
169
+ isHiddenFile(
170
+ currentFile.startsWith("/")
171
+ ? currentFile.slice(1)
172
+ : currentFile,
173
+ ) && (
174
+ <SelectItem className="py-1" value={currentFile}>
175
+ <span
176
+ className={`text-xs pr-1 block truncate ${
177
+ sidebarOpen ? "max-w-[5rem]" : "max-w-[10rem]"
178
+ }`}
179
+ >
180
+ {currentFile}
181
+ </span>
182
+ </SelectItem>
183
+ )}
184
+ </SelectContent>
185
+ </Select>
186
+ </div>
89
187
 
90
- <div className="flex items-center gap-2 px-2 py-1 ml-auto">
188
+ <div className="flex items-center overflow-x-hidden gap-2 px-2 py-1 ml-auto">
91
189
  {checkIfManualEditsImported(files) && (
92
190
  <DropdownMenu>
93
191
  <DropdownMenuTrigger asChild>
@@ -112,18 +210,6 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
112
210
  </DropdownMenuContent>
113
211
  </DropdownMenu>
114
212
  )}
115
- <DropdownMenu>
116
- <DropdownMenuTrigger asChild>
117
- <Button size="sm" variant="ghost">
118
- Insert
119
- </Button>
120
- </DropdownMenuTrigger>
121
- <DropdownMenuContent>
122
- <DropdownMenuItem onClick={() => setFootprintDialogOpen(true)}>
123
- Chip
124
- </DropdownMenuItem>
125
- </DropdownMenuContent>
126
- </DropdownMenu>
127
213
  <Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
128
214
  Import
129
215
  </Button>
@@ -137,14 +223,6 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
137
223
  updateFileContent(currentFile, newContent)
138
224
  }}
139
225
  />
140
- <FootprintDialog
141
- currentFile={currentFile as `${string}.${string}`}
142
- open={footprintDialogOpen}
143
- onOpenChange={setFootprintDialogOpen}
144
- updateFileContent={updateFileContent}
145
- files={files}
146
- cursorPosition={cursorPosition}
147
- />
148
226
  </div>
149
227
  </>
150
228
  )
@@ -218,14 +218,16 @@ export default function EditorNav({
218
218
  {pkg.star_count}
219
219
  </span>
220
220
  )}
221
- <Button
222
- variant="ghost"
223
- size="icon"
224
- className="h-6 w-6 ml-2"
225
- onClick={() => openRenameDialog()}
226
- >
227
- <Pencil className="h-3 w-3 text-gray-700" />
228
- </Button>
221
+ {pkg.owner_github_username === session?.github_username && (
222
+ <Button
223
+ variant="ghost"
224
+ size="icon"
225
+ className="h-6 w-6 ml-2"
226
+ onClick={() => openRenameDialog()}
227
+ >
228
+ <Pencil className="h-3 w-3 text-gray-700" />
229
+ </Button>
230
+ )}
229
231
  {isPrivate && (
230
232
  <div className="relative group">
231
233
  <LockClosedIcon className="h-3 w-3 text-gray-700" />
@@ -9,7 +9,7 @@ const treeVariants = cva(
9
9
  )
10
10
 
11
11
  const selectedTreeVariants = cva(
12
- "before:opacity-100 before:bg-slate-100/70 text-accent-foreground' dark:before:bg-slate-800/70",
12
+ "before:opacity-100 before:bg-slate-100/70 text-accent-foreground dark:before:bg-slate-800/70",
13
13
  )
14
14
 
15
15
  const dragOverVariants = cva(
@@ -58,6 +58,10 @@ const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
58
58
  string | undefined
59
59
  >(initialSelectedItemId)
60
60
 
61
+ React.useEffect(() => {
62
+ setSelectedItemId(initialSelectedItemId)
63
+ }, [initialSelectedItemId])
64
+
61
65
  const [draggedItem, setDraggedItem] = React.useState<TreeDataItem | null>(
62
66
  null,
63
67
  )
@@ -1,6 +1,8 @@
1
- // Prettier is injected into the global scope inside index.html
2
1
  declare global {
3
2
  interface Window {
3
+ TSCIRCUIT_REGISTRY_API_BASE_URL: string
4
+ TSCIRCUIT_3D_OBJECT_REF: any
5
+ __DEBUG_CODE_EDITOR_FS_MAP: Map<string, string>
4
6
  prettier: {
5
7
  format: (code: string, options: any) => string
6
8
  }
@@ -0,0 +1,62 @@
1
+ import { PackageFile } from "@/components/package-port/CodeAndPreview"
2
+
3
+ export const findMainEntrypointFileFromTscircuitConfig = (
4
+ files: PackageFile[],
5
+ ): PackageFile | null => {
6
+ const configFile = files.find((file) => file.path === "tscircuit.config.json")
7
+
8
+ if (configFile) {
9
+ try {
10
+ const config = JSON.parse(configFile.content)
11
+
12
+ if (config && typeof config.mainEntrypoint === "string") {
13
+ const mainComponentPath = config.mainEntrypoint
14
+
15
+ const normalizedPath = mainComponentPath.startsWith("./")
16
+ ? mainComponentPath.substring(2)
17
+ : mainComponentPath
18
+
19
+ return files.find((file) => file.path === normalizedPath) ?? null
20
+ }
21
+ } catch {}
22
+ }
23
+
24
+ return null
25
+ }
26
+
27
+ export const findTargetFile = (
28
+ files: PackageFile[],
29
+ filePathFromUrl: string | null,
30
+ ): PackageFile | null => {
31
+ if (files.length === 0) {
32
+ return null
33
+ }
34
+
35
+ let targetFile: PackageFile | null = null
36
+
37
+ if (filePathFromUrl) {
38
+ targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
39
+ }
40
+
41
+ if (!targetFile) {
42
+ targetFile = findMainEntrypointFileFromTscircuitConfig(files)
43
+ }
44
+
45
+ if (!targetFile) {
46
+ targetFile = files.find((file) => file.path === "index.tsx") ?? null
47
+ }
48
+
49
+ if (!targetFile) {
50
+ targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
51
+ }
52
+
53
+ if (!targetFile) {
54
+ targetFile = files.find((file) => file.path === "index.ts") ?? null
55
+ }
56
+
57
+ if (!targetFile && files[0]) {
58
+ targetFile = files[0]
59
+ }
60
+
61
+ return targetFile
62
+ }
@@ -6,6 +6,9 @@ export async function loadPrettier() {
6
6
  loadScript(
7
7
  "https://cdn.jsdelivr.net/npm/prettier@2.8.8/parser-typescript.js",
8
8
  ),
9
+ loadScript(
10
+ "https://cdn.jsdelivr.net/npm/prettier@2.8.8/parser-markdown.js",
11
+ ),
9
12
  ])
10
13
  }
11
14
 
package/src/pages/404.tsx CHANGED
@@ -1,9 +1,7 @@
1
- import React from "react"
2
1
  import { Helmet } from "react-helmet"
3
2
  import { Header2 } from "@/components/Header2"
4
3
  import Footer from "@/components/Footer"
5
- import { Button } from "@/components/ui/button"
6
- import { PrefetchPageLink } from "@/components/PrefetchPageLink"
4
+ import { NotFound } from "@/components/NotFound"
7
5
 
8
6
  export function NotFoundPage({
9
7
  heading = "Page Not Found",
@@ -18,36 +16,7 @@ export function NotFoundPage({
18
16
  />
19
17
  </Helmet>
20
18
  <Header2 />
21
- <main className="flex-1 flex items-center justify-center min-h-[90vh]">
22
- <div className="container px-4 md:px-6 py-12 flex flex-col items-center text-center max-w-3xl">
23
- <div className="mb-8 flex flex-col items-center justify-center">
24
- <div className="mb-2">
25
- <span className="text-3xl font-bold text-white bg-blue-500 px-4 py-2 rounded-md shadow-md inline-block">
26
- 404
27
- </span>
28
- </div>
29
- </div>
30
- <h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl mb-4">
31
- {heading}
32
- </h1>
33
- <p className="text-xl text-muted-foreground mb-8">
34
- The page you're looking for doesn't exist or has been moved to
35
- another address.
36
- </p>
37
- <div className="flex flex-col sm:flex-row gap-4">
38
- <PrefetchPageLink href="/">
39
- <Button size="lg" className="bg-blue-500 hover:bg-blue-600">
40
- Return Home
41
- </Button>
42
- </PrefetchPageLink>
43
- <PrefetchPageLink href="/search">
44
- <Button size="lg" variant="outline">
45
- Search Packages
46
- </Button>
47
- </PrefetchPageLink>
48
- </div>
49
- </div>
50
- </main>
19
+ <NotFound heading={heading} />
51
20
  <Footer />
52
21
  </div>
53
22
  )
@@ -4,10 +4,15 @@ import Header from "@/components/Header"
4
4
  import { usePackage } from "@/hooks/use-package"
5
5
  import { Helmet } from "react-helmet-async"
6
6
  import { useCurrentPackageId } from "@/hooks/use-current-package-id"
7
+ import { NotFound } from "@/components/NotFound"
8
+ import { ErrorOutline } from "@/components/ErrorOutline"
7
9
 
8
10
  export const EditorPage = () => {
9
11
  const { packageId } = useCurrentPackageId()
10
12
  const { data: pkg, isLoading, error } = usePackage(packageId)
13
+ const uuid4RegExp = new RegExp(
14
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
15
+ )
11
16
  return (
12
17
  <div className="overflow-x-hidden">
13
18
  <Helmet>
@@ -32,10 +37,16 @@ export const EditorPage = () => {
32
37
  </Helmet>
33
38
  <Header />
34
39
  {!error && <CodeAndPreview pkg={pkg} />}
35
- {error && error.status === 404 && <h1>"Package Not Found"</h1>}
40
+ {error &&
41
+ (error.status === 404 || !uuid4RegExp.test(packageId ?? "")) && (
42
+ <NotFound heading="Package not found" />
43
+ )}
36
44
  {error && error.status !== 404 && (
37
- <div className="flex flex-col">
38
- Something strange happened<div>{error.message}</div>
45
+ <div className="min-h-screen grid place-items-center">
46
+ <ErrorOutline
47
+ error={error}
48
+ description={"There was an error loading the editor page"}
49
+ />
39
50
  </div>
40
51
  )}
41
52
  <Footer />