@tscircuit/fake-snippets 0.0.108 → 0.0.109

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 (57) hide show
  1. package/bun.lock +62 -19
  2. package/dist/bundle.js +3 -2
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.js +2 -1
  5. package/dist/schema.d.ts +8 -0
  6. package/dist/schema.js +2 -1
  7. package/fake-snippets-api/lib/db/schema.ts +1 -0
  8. package/package.json +7 -8
  9. package/src/App.tsx +0 -2
  10. package/src/components/DownloadButtonAndMenu.tsx +133 -35
  11. package/src/components/FileSidebar.tsx +31 -34
  12. package/src/components/Footer.tsx +0 -1
  13. package/src/components/HeaderLogin.tsx +1 -1
  14. package/src/components/HiddenFilesDropdown.tsx +0 -2
  15. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
  16. package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
  17. package/src/components/PackageCard.tsx +0 -1
  18. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  19. package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
  20. package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
  21. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
  22. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +0 -1
  23. package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
  24. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
  25. package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
  26. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  27. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  28. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  29. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  30. package/src/components/package-port/CodeAndPreview.tsx +23 -40
  31. package/src/components/package-port/CodeEditor.tsx +24 -1
  32. package/src/components/package-port/CodeEditorHeader.tsx +5 -2
  33. package/src/components/preview/PackageReleasesDashboard.tsx +30 -25
  34. package/src/hooks/use-current-package-id.ts +5 -30
  35. package/src/hooks/use-current-package-info.ts +29 -5
  36. package/src/hooks/use-global-store.ts +1 -1
  37. package/src/hooks/useFileManagement.ts +153 -34
  38. package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
  39. package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
  40. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  41. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  42. package/src/lib/utils/isComponentExported.ts +9 -0
  43. package/src/pages/authorize.tsx +0 -2
  44. package/src/pages/landing.tsx +0 -1
  45. package/src/pages/preview-release.tsx +14 -4
  46. package/src/pages/view-package.tsx +14 -13
  47. package/src/components/Footer2.tsx +0 -100
  48. package/src/components/ShippingInformationForm.tsx +0 -423
  49. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  50. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  51. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  52. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  53. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  54. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  55. package/src/components/ViewSnippetHeader.tsx +0 -181
  56. package/src/components/ui/input-otp.tsx +0 -69
  57. package/src/pages/settings.tsx +0 -25
@@ -43,7 +43,7 @@ export const HeaderLogin = () => {
43
43
  </AvatarFallback>
44
44
  </Avatar>
45
45
  </DropdownMenuTrigger>
46
- <DropdownMenuContent className="ml-1 md:ml-0 md:mr-1">
46
+ <DropdownMenuContent className="ml-1 mr-1 md:ml-0 md:mr-1">
47
47
  <DropdownMenuItem asChild className="text-gray-500 text-xs" disabled>
48
48
  <div>
49
49
  AI Usage $
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import {
4
2
  DropdownMenu,
5
3
  DropdownMenuContent,
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
4
2
  import { PackageRelease } from "fake-snippets-api/lib/db/schema"
5
3
  import { useState } from "react"
@@ -1,9 +1,13 @@
1
1
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
2
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
+ import { useState } from "react"
4
+ import { CircuitBoard } from "lucide-react"
3
5
 
4
6
  export function BuildPreviewContent() {
5
7
  const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
6
8
  const { packageInfo } = useCurrentPackageInfo()
9
+ const [imageError, setImageError] = useState(false)
10
+ const [imageLoading, setImageLoading] = useState(true)
7
11
 
8
12
  if (!packageRelease) {
9
13
  return (
@@ -16,11 +20,36 @@ export function BuildPreviewContent() {
16
20
  return (
17
21
  <div className="flex items-center justify-center w-full h-full">
18
22
  <div className="rounded overflow-hidden w-full max-w-full">
19
- <img
20
- src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
21
- alt="Package build preview"
22
- className="object-contain rounded w-full h-auto max-h-[240px] sm:max-h-[300px] lg:max-h-[360px]"
23
- />
23
+ {imageError ? (
24
+ <div className="flex flex-col items-center justify-center bg-gray-50 border border-gray-300 rounded-lg p-8 sm:p-12 lg:p-16 min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
25
+ <CircuitBoard className="w-12 h-12 sm:w-16 sm:h-16 lg:w-20 lg:h-20 text-gray-400 mb-4" />
26
+ <h3 className="text-lg sm:text-xl font-medium text-gray-600 mb-2">
27
+ Preview Not Available
28
+ </h3>
29
+ <p className="text-sm sm:text-base text-gray-500 text-center max-w-sm">
30
+ The build preview image could not be loaded. This may be because
31
+ the build is still processing or the image is not available.
32
+ </p>
33
+ </div>
34
+ ) : (
35
+ <>
36
+ {imageLoading && (
37
+ <div className="flex items-center justify-center bg-gray-100 rounded-lg min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
38
+ <div className="w-16 h-16 sm:w-20 sm:h-20 bg-gray-200 rounded animate-pulse"></div>
39
+ </div>
40
+ )}
41
+ <img
42
+ src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
43
+ alt="Package build preview"
44
+ className={`object-contain rounded w-full h-auto max-h-[240px] sm:max-h-[300px] lg:max-h-[360px] ${imageLoading ? "hidden" : "block"}`}
45
+ onLoad={() => setImageLoading(false)}
46
+ onError={() => {
47
+ setImageError(true)
48
+ setImageLoading(false)
49
+ }}
50
+ />
51
+ </>
52
+ )}
24
53
  </div>
25
54
  </div>
26
55
  )
@@ -19,7 +19,6 @@ import {
19
19
  } from "@/components/ui/dropdown-menu"
20
20
  import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
21
21
  import { timeAgo } from "@/lib/utils/timeAgo"
22
- import { ImageWithFallback } from "./ImageWithFallback"
23
22
  import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
24
23
 
25
24
  export interface PackageCardProps {
@@ -1,11 +1,17 @@
1
1
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
2
- import { useEffect, useMemo } from "react"
3
- import { useQuery } from "react-query"
2
+ import { useMemo } from "react"
4
3
  import { Skeleton } from "@/components/ui/skeleton"
5
4
 
6
5
  // Pre-randomized array to avoid flickering on re-renders
7
- const SKELETON_WIDTHS = ["w-2/3", "w-1/4", "w-5/6", "w-1/3", "w-1/2", "w-3/4"]
8
-
6
+ export const SKELETON_WIDTHS = [
7
+ "w-2/3",
8
+ "w-1/4",
9
+ "w-5/6",
10
+ "w-1/3",
11
+ "w-1/2",
12
+ "w-3/4",
13
+ ]
14
+ const PLACEHOLDER_SHIKI_HTML = `<pre class="shiki vitesse-light" style="background-color:#ffffff;color:#393a34" tabindex="0"><code><span class="line"></span></code></pre>`
9
15
  export const ShikiCodeViewer = ({
10
16
  code,
11
17
  filePath,
@@ -24,17 +30,20 @@ export const ShikiCodeViewer = ({
24
30
  [filePath, code, highlighter],
25
31
  )
26
32
 
27
- if (!html) {
33
+ if (html && html?.trim() !== PLACEHOLDER_SHIKI_HTML) {
28
34
  return (
29
- <div className="text-sm p-4">
30
- {SKELETON_WIDTHS.map((w, i) => (
31
- <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
32
- ))}
33
- </div>
35
+ <div
36
+ className="text-sm shiki"
37
+ dangerouslySetInnerHTML={{ __html: html }}
38
+ />
34
39
  )
35
40
  }
36
41
 
37
42
  return (
38
- <div className="text-sm shiki" dangerouslySetInnerHTML={{ __html: html }} />
43
+ <div className="text-sm p-4">
44
+ {SKELETON_WIDTHS.map((w, i) => (
45
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
46
+ ))}
47
+ </div>
39
48
  )
40
49
  }
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState, useEffect, useMemo, useCallback } from "react"
4
2
  import {
5
3
  Edit,
@@ -14,7 +12,7 @@ import {
14
12
  import { Skeleton } from "@/components/ui/skeleton"
15
13
  import { Button } from "@/components/ui/button"
16
14
  import { usePackageFile } from "@/hooks/use-package-files"
17
- import { ShikiCodeViewer } from "./ShikiCodeViewer"
15
+ import { ShikiCodeViewer, SKELETON_WIDTHS } from "./ShikiCodeViewer"
18
16
  import MarkdownViewer from "./markdown-viewer"
19
17
  import { useGlobalStore } from "@/hooks/use-global-store"
20
18
  import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
@@ -24,12 +22,12 @@ interface PackageFile {
24
22
  package_release_id: string
25
23
  file_path: string
26
24
  created_at: string
27
- content_text?: string
25
+ content_text?: string | null
28
26
  }
29
27
 
30
28
  interface ImportantFilesViewProps {
31
29
  importantFiles?: PackageFile[]
32
- isLoading?: boolean
30
+ isFetched?: boolean
33
31
  onEditClicked?: (file_path?: string | null) => void
34
32
  packageAuthorOwner?: string | null
35
33
  aiDescription?: string
@@ -56,7 +54,7 @@ export default function ImportantFilesView({
56
54
  aiReviewText,
57
55
  aiReviewRequested,
58
56
  onRequestAiReview,
59
- isLoading = false,
57
+ isFetched = false,
60
58
  onEditClicked,
61
59
  packageAuthorOwner,
62
60
  onLicenseFileRequested,
@@ -130,19 +128,25 @@ export default function ImportantFilesView({
130
128
  const availableTabs = useMemo((): TabInfo[] => {
131
129
  const tabs: TabInfo[] = []
132
130
 
133
- tabs.push({
134
- type: "ai",
135
- filePath: null,
136
- label: "Description",
137
- icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
138
- })
131
+ // Only show AI description tab if there's actual AI content
132
+ if (hasAiContent) {
133
+ tabs.push({
134
+ type: "ai",
135
+ filePath: null,
136
+ label: "Description",
137
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
138
+ })
139
+ }
139
140
 
140
- tabs.push({
141
- type: "ai-review",
142
- filePath: null,
143
- label: "AI Review",
144
- icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
145
- })
141
+ // Only show AI review tab if there's actual AI review content
142
+ if (hasAiReview || isOwner) {
143
+ tabs.push({
144
+ type: "ai-review",
145
+ filePath: null,
146
+ label: "AI Review",
147
+ icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
148
+ })
149
+ }
146
150
 
147
151
  importantFiles.forEach((file) => {
148
152
  tabs.push({
@@ -154,11 +158,11 @@ export default function ImportantFilesView({
154
158
  })
155
159
 
156
160
  return tabs
157
- }, [hasAiContent, importantFiles, getFileName, getFileIcon])
161
+ }, [hasAiContent, hasAiReview, importantFiles, getFileName, getFileIcon])
158
162
 
159
163
  // Find default tab with fallback logic
160
164
  const getDefaultTab = useCallback((): TabInfo | null => {
161
- if (isLoading || availableTabs.length === 0) return null
165
+ if (!isFetched || availableTabs.length === 0) return null
162
166
 
163
167
  // Priority 1: README file
164
168
  const readmeTab = availableTabs.find(
@@ -167,12 +171,14 @@ export default function ImportantFilesView({
167
171
  )
168
172
  if (readmeTab) return readmeTab
169
173
 
170
- // Priority 2: AI content
171
- const aiTab = availableTabs.find((tab) => tab.type === "ai")
174
+ // Priority 2: AI content (only if available)
175
+ const aiTab = availableTabs.find((tab) => tab.type === "ai" && hasAiContent)
172
176
  if (aiTab) return aiTab
173
177
 
174
178
  // Priority 3: AI review
175
- const aiReviewTab = availableTabs.find((tab) => tab.type === "ai-review")
179
+ const aiReviewTab = availableTabs.find(
180
+ (tab) => tab.type === "ai-review" && hasAiReview,
181
+ )
176
182
  if (aiReviewTab) return aiReviewTab
177
183
 
178
184
  // Priority 4: First file
@@ -180,24 +186,7 @@ export default function ImportantFilesView({
180
186
  if (firstFileTab) return firstFileTab
181
187
 
182
188
  return null
183
- }, [isLoading, availableTabs, isReadmeFile])
184
-
185
- // Handle copy functionality
186
- const handleCopy = useCallback(() => {
187
- let textToCopy = ""
188
-
189
- if (activeTab?.type === "ai-review" && aiReviewText) {
190
- textToCopy = aiReviewText
191
- } else if (activeTab?.type === "file" && activeFileContent) {
192
- textToCopy = activeFileContent
193
- }
194
-
195
- if (textToCopy) {
196
- navigator.clipboard.writeText(textToCopy)
197
- setCopyState("copied")
198
- setTimeout(() => setCopyState("copy"), 500)
199
- }
200
- }, [activeTab, aiReviewText])
189
+ }, [isFetched, availableTabs, isReadmeFile])
201
190
 
202
191
  // Handle tab selection with validation
203
192
  const selectTab = useCallback(
@@ -245,11 +234,11 @@ export default function ImportantFilesView({
245
234
 
246
235
  // Set default tab when no tab is active
247
236
  useEffect(() => {
248
- if (activeTab === null && !isLoading) {
237
+ if (activeTab === null && isFetched) {
249
238
  const defaultTab = getDefaultTab()
250
239
  setActiveTab(defaultTab)
251
240
  }
252
- }, [activeTab, isLoading, getDefaultTab])
241
+ }, [activeTab, isFetched, getDefaultTab])
253
242
 
254
243
  // Validate active tab still exists (handles file deletion)
255
244
  useEffect(() => {
@@ -273,18 +262,41 @@ export default function ImportantFilesView({
273
262
  return importantFiles.find((file) => file.file_path === activeTab.filePath)
274
263
  }, [activeTab, importantFiles])
275
264
 
276
- const { data: activeFileFull } = usePackageFile(
277
- partialActiveFile
278
- ? {
279
- file_path: partialActiveFile.file_path,
280
- package_release_id: partialActiveFile.package_release_id,
281
- }
282
- : null,
283
- { keepPreviousData: true },
284
- )
265
+ const { data: activeFileFull, isFetched: isActiveFileFetched } =
266
+ usePackageFile(
267
+ partialActiveFile
268
+ ? {
269
+ file_path: partialActiveFile.file_path,
270
+ package_release_id: partialActiveFile.package_release_id,
271
+ }
272
+ : null,
273
+ {
274
+ keepPreviousData: true,
275
+ staleTime: Infinity,
276
+ refetchOnMount: false,
277
+ refetchOnWindowFocus: false,
278
+ refetchOnReconnect: false,
279
+ },
280
+ )
285
281
 
286
282
  const activeFileContent = activeFileFull?.content_text || ""
287
283
 
284
+ const handleCopy = () => {
285
+ let textToCopy = ""
286
+
287
+ if (activeTab?.type === "ai-review" && aiReviewText) {
288
+ textToCopy = aiReviewText
289
+ } else if (activeTab?.type === "file" && activeFileContent) {
290
+ textToCopy = activeFileContent
291
+ }
292
+
293
+ if (textToCopy) {
294
+ navigator.clipboard.writeText(textToCopy)
295
+ setCopyState("copied")
296
+ setTimeout(() => setCopyState("copy"), 500)
297
+ }
298
+ }
299
+
288
300
  // Render content based on active tab
289
301
  const renderAiContent = useCallback(
290
302
  () => (
@@ -369,20 +381,24 @@ export default function ImportantFilesView({
369
381
  }, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
370
382
 
371
383
  const renderFileContent = useCallback(() => {
372
- if (!activeTab?.filePath || !activeFileContent) {
373
- return <pre className="whitespace-pre-wrap">{activeFileContent}</pre>
384
+ if (!isActiveFileFetched || !activeTab?.filePath || !activeFileContent) {
385
+ ;<div className="text-sm p-4">
386
+ {SKELETON_WIDTHS.map((w, i) => (
387
+ <Skeleton key={i} className={`h-4 mb-2 ${w}`} />
388
+ ))}
389
+ </div>
374
390
  }
375
391
 
376
- if (isMarkdownFile(activeTab.filePath)) {
392
+ if (isMarkdownFile(String(activeTab?.filePath))) {
377
393
  return <MarkdownViewer markdownContent={activeFileContent} />
378
394
  }
379
395
 
380
- if (isCodeFile(activeTab.filePath)) {
396
+ if (isCodeFile(String(activeTab?.filePath))) {
381
397
  return (
382
- <div className="overflow-x-auto">
398
+ <div className="overflow-x-auto no-scrollbar">
383
399
  <ShikiCodeViewer
384
400
  code={activeFileContent}
385
- filePath={activeTab.filePath}
401
+ filePath={String(activeTab?.filePath)}
386
402
  />
387
403
  </div>
388
404
  )
@@ -422,7 +438,7 @@ export default function ImportantFilesView({
422
438
  [activeTab],
423
439
  )
424
440
 
425
- if (isLoading) {
441
+ if (!isFetched) {
426
442
  return (
427
443
  <div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
428
444
  <div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState } from "react"
4
2
  import { Button } from "@/components/ui/button"
5
3
  import {
@@ -72,7 +70,9 @@ export default function MainContentHeader({
72
70
  }
73
71
  }
74
72
 
75
- const { circuitJson } = useCurrentPackageCircuitJson()
73
+ const hasCircuitJson = packageFiles.some(
74
+ (file) => file.file_path === "dist/circuit.json",
75
+ )
76
76
 
77
77
  return (
78
78
  <div className="flex items-center justify-between mb-4">
@@ -86,7 +86,7 @@ export default function MainContentHeader({
86
86
  unscopedName={packageInfo?.unscoped_name}
87
87
  desiredImageType={activeView}
88
88
  author={packageInfo?.owner_github_username ?? undefined}
89
- circuitJson={circuitJson}
89
+ hasCircuitJson={hasCircuitJson}
90
90
  />
91
91
 
92
92
  {/* Code Dropdown */}
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import React from "react"
3
2
  import {
4
3
  Code,
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import { GitFork, Star, Tag, Settings, LinkIcon } from "lucide-react"
3
2
  import { Badge } from "@/components/ui/badge"
4
3
  import { Skeleton } from "@/components/ui/skeleton"
@@ -12,10 +12,7 @@ import { Lock, Globe } from "lucide-react"
12
12
  import { GitFork, Package, Star } from "lucide-react"
13
13
 
14
14
  import { useForkPackageMutation } from "@/hooks/use-fork-package-mutation"
15
- import {
16
- usePackageStarMutationByName,
17
- usePackageStarsByName,
18
- } from "@/hooks/use-package-stars"
15
+ import { usePackageStarMutationByName } from "@/hooks/use-package-stars"
19
16
  import { useOrderDialog } from "@tscircuit/runframe"
20
17
  import { useGlobalStore } from "@/hooks/use-global-store"
21
18
  import { Package as PackageType } from "fake-snippets-api/lib/db/schema"
@@ -45,8 +42,7 @@ export default function PackageHeader({
45
42
  isLoggedIn,
46
43
  packageReleaseId: packageInfo?.latest_package_release_id ?? "",
47
44
  })
48
- const { data: starData, isLoading: isStarDataLoading } =
49
- usePackageStarsByName(packageInfo?.name ?? null)
45
+
50
46
  const { addStar, removeStar } = usePackageStarMutationByName(
51
47
  packageInfo?.name ?? "",
52
48
  )
@@ -57,7 +53,7 @@ export default function PackageHeader({
57
53
  const handleStarClick = async () => {
58
54
  if (!packageInfo?.name || !isLoggedIn) return
59
55
 
60
- if (starData?.is_starred) {
56
+ if (packageInfo?.is_starred) {
61
57
  await removeStar.mutateAsync()
62
58
  } else {
63
59
  await addStar.mutateAsync()
@@ -69,8 +65,7 @@ export default function PackageHeader({
69
65
  await forkPackage(packageInfo.package_id)
70
66
  }
71
67
 
72
- const isStarLoading =
73
- isStarDataLoading || addStar.isLoading || removeStar.isLoading
68
+ const isStarLoading = addStar.isLoading || removeStar.isLoading
74
69
 
75
70
  useEffect(() => {
76
71
  window.TSCIRCUIT_REGISTRY_API_BASE_URL =
@@ -155,15 +150,15 @@ export default function PackageHeader({
155
150
  >
156
151
  <Star
157
152
  className={`w-4 h-4 mr-2 ${
158
- starData?.is_starred
153
+ packageInfo?.is_starred
159
154
  ? "fill-yellow-500 text-yellow-500"
160
155
  : ""
161
156
  }`}
162
157
  />
163
- {starData?.is_starred ? "Starred" : "Star"}
164
- {(starData?.star_count ?? 0) > 0 && (
158
+ {packageInfo?.is_starred ? "Starred" : "Star"}
159
+ {(packageInfo?.star_count ?? 0) > 0 && (
165
160
  <span className="ml-1.5 bg-gray-100 text-gray-700 rounded-full px-1.5 py-0.5 text-xs font-medium">
166
- {starData?.star_count}
161
+ {packageInfo?.star_count}
167
162
  </span>
168
163
  )}
169
164
  </Button>
@@ -229,13 +224,15 @@ export default function PackageHeader({
229
224
  >
230
225
  <Star
231
226
  className={`w-4 h-4 mr-2 ${
232
- starData?.is_starred ? "fill-yellow-500 text-yellow-500" : ""
227
+ packageInfo?.is_starred
228
+ ? "fill-yellow-500 text-yellow-500"
229
+ : ""
233
230
  }`}
234
231
  />
235
- {starData?.is_starred ? "Starred" : "Star"}
236
- {(starData?.star_count ?? 0) > 0 && (
232
+ {packageInfo?.is_starred ? "Starred" : "Star"}
233
+ {(packageInfo?.star_count ?? 0) > 0 && (
237
234
  <span className="ml-1.5 bg-gray-100 text-gray-700 rounded-full px-1.5 py-0.5 text-xs font-medium">
238
- {starData?.star_count}
235
+ {packageInfo?.star_count}
239
236
  </span>
240
237
  )}
241
238
  </Button>
@@ -1,4 +1,3 @@
1
- "use client"
2
1
  import { Skeleton } from "@/components/ui/skeleton"
3
2
  import { usePreviewImages } from "@/hooks/use-preview-images"
4
3
  import type { Package } from "fake-snippets-api/lib/db/schema"
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { useState, useEffect, useMemo } from "react"
4
2
  import MainContentHeader from "./main-content-header"
5
3
  import Sidebar from "./sidebar"
@@ -21,20 +19,18 @@ import Footer from "@/components/Footer"
21
19
  import PackageHeader from "./package-header"
22
20
  import { useGlobalStore } from "@/hooks/use-global-store"
23
21
  import { useLocation } from "wouter"
24
- import { Package } from "fake-snippets-api/lib/db/schema"
25
- import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
22
+ import type {
23
+ Package,
24
+ PackageFile as ApiPackageFile,
25
+ } from "fake-snippets-api/lib/db/schema"
26
26
  import { useRequestAiReviewMutation } from "@/hooks/use-request-ai-review-mutation"
27
27
  import { useAiReview } from "@/hooks/use-ai-review"
28
28
  import { useQueryClient } from "react-query"
29
29
  import SidebarReleasesSection from "./sidebar-releases-section"
30
30
 
31
- interface PackageFile {
32
- package_file_id: string
33
- package_release_id: string
34
- file_path: string
35
- file_content: string
36
- content_text?: string // Keep for backward compatibility
37
- created_at: string // iso-8601
31
+ interface PackageFile extends ApiPackageFile {
32
+ file_content?: string
33
+ content_text?: string | null // Keep for backward compatibility
38
34
  }
39
35
 
40
36
  interface RepoPageContentProps {
@@ -44,10 +40,12 @@ interface RepoPageContentProps {
44
40
  packageRelease?: import("fake-snippets-api/lib/db/schema").PackageRelease
45
41
  onFileClicked?: (file: PackageFile) => void
46
42
  onEditClicked?: () => void
43
+ arePackageFilesFetched?: boolean
47
44
  }
48
45
 
49
46
  export default function RepoPageContent({
50
47
  packageFiles,
48
+ arePackageFilesFetched = false,
51
49
  packageInfo,
52
50
  packageRelease,
53
51
  onFileClicked,
@@ -70,8 +68,12 @@ export default function RepoPageContent({
70
68
  }
71
69
  }, [aiReview?.ai_review_text, queryClient])
72
70
  const session = useGlobalStore((s) => s.session)
73
- const { circuitJson, isLoading: isCircuitJsonLoading } =
74
- useCurrentPackageCircuitJson()
71
+
72
+ // Check if circuit.json exists without downloading it
73
+ const circuitJsonExists = useMemo(() => {
74
+ return packageFiles?.some((file) => file.file_path === "dist/circuit.json")
75
+ }, [packageFiles])
76
+
75
77
  const { mutate: requestAiReview, isLoading: isRequestingAiReview } =
76
78
  useRequestAiReviewMutation({
77
79
  onSuccess: (_packageRelease, aiReview) => {
@@ -91,13 +93,12 @@ export default function RepoPageContent({
91
93
 
92
94
  // Handle initial view selection and hash-based view changes
93
95
  useEffect(() => {
94
- if (isCircuitJsonLoading) return
95
- if (!packageInfo) return
96
+ if (!packageInfo || !arePackageFilesFetched) return
96
97
  const hash = window.location.hash.slice(1)
97
98
  const validViews = ["files", "3d", "pcb", "schematic", "bom"]
98
99
  const circuitDependentViews = ["3d", "pcb", "schematic", "bom"]
99
100
 
100
- const availableViews = circuitJson
101
+ const availableViews = circuitJsonExists
101
102
  ? validViews
102
103
  : validViews.filter((view) => !circuitDependentViews.includes(view))
103
104
 
@@ -115,7 +116,7 @@ export default function RepoPageContent({
115
116
  window.location.hash = "files"
116
117
  }
117
118
  }
118
- }, [packageInfo?.default_view, circuitJson, isCircuitJsonLoading])
119
+ }, [packageInfo?.default_view, circuitJsonExists])
119
120
 
120
121
  const importantFilePaths = packageFiles
121
122
  ?.filter((pf) => isPackageFileImportant(pf.file_path))
@@ -146,7 +147,7 @@ export default function RepoPageContent({
146
147
  return (
147
148
  <FilesView
148
149
  packageFiles={packageFiles}
149
- isLoading={!packageFiles}
150
+ arePackageFilesFetched={arePackageFilesFetched}
150
151
  onFileClicked={onFileClicked}
151
152
  />
152
153
  )
@@ -162,7 +163,7 @@ export default function RepoPageContent({
162
163
  return (
163
164
  <FilesView
164
165
  packageFiles={packageFiles}
165
- isLoading={!packageFiles}
166
+ arePackageFilesFetched={arePackageFilesFetched}
166
167
  onFileClicked={onFileClicked}
167
168
  />
168
169
  )
@@ -215,7 +216,7 @@ export default function RepoPageContent({
215
216
  {/* Important Files View - Always shown */}
216
217
  <ImportantFilesView
217
218
  importantFiles={importantFiles}
218
- isLoading={!packageFiles}
219
+ isFetched={arePackageFilesFetched}
219
220
  onEditClicked={onEditClicked}
220
221
  packageAuthorOwner={packageInfo?.owner_github_username}
221
222
  aiDescription={packageInfo?.ai_description ?? ""}
@@ -1,5 +1,3 @@
1
- "use client"
2
-
3
1
  import { Package } from "fake-snippets-api/lib/db/schema"
4
2
  import SidebarAboutSection from "./sidebar-about-section"
5
3
  import SidebarReleasesSection from "./sidebar-releases-section"