@tscircuit/fake-snippets 0.0.27 → 0.0.28

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 (55) hide show
  1. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +5 -6
  2. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +3 -0
  3. package/bun-tests/fake-snippets-api/routes/packages/list-2.test.ts +2 -0
  4. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +5 -3
  5. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +8 -5
  6. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +1 -1
  7. package/bun.lock +110 -5
  8. package/dist/bundle.js +14 -9
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +4 -2
  11. package/fake-snippets-api/lib/db/autoload-snippets.json +4 -0
  12. package/fake-snippets-api/lib/db/db-client.ts +2 -1
  13. package/fake-snippets-api/lib/db/schema.ts +1 -0
  14. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +1 -0
  15. package/fake-snippets-api/routes/api/package_files/list.ts +6 -3
  16. package/fake-snippets-api/routes/api/packages/create.ts +2 -1
  17. package/fake-snippets-api/routes/api/snippets/create.ts +1 -0
  18. package/package.json +2 -1
  19. package/public/placeholder-logo.png +0 -0
  20. package/public/placeholder-logo.svg +1 -0
  21. package/public/placeholder-user.jpg +0 -0
  22. package/public/placeholder.jpg +0 -0
  23. package/public/placeholder.svg +1 -0
  24. package/src/App.tsx +5 -0
  25. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +50 -0
  26. package/src/components/ViewPackagePage/components/file-explorer.tsx +118 -0
  27. package/src/components/ViewPackagePage/components/important-files-view.tsx +231 -0
  28. package/src/components/ViewPackagePage/components/main-content-header.tsx +172 -0
  29. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +106 -0
  30. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +130 -0
  31. package/src/components/ViewPackagePage/components/package-header.tsx +107 -0
  32. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +63 -0
  33. package/src/components/ViewPackagePage/components/readme-view.tsx +58 -0
  34. package/src/components/ViewPackagePage/components/repo-header-button.tsx +36 -0
  35. package/src/components/ViewPackagePage/components/repo-header.tsx +4 -0
  36. package/src/components/ViewPackagePage/components/repo-page-content.tsx +213 -0
  37. package/src/components/ViewPackagePage/components/repo-tab-header.tsx +12 -0
  38. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +103 -0
  39. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +31 -0
  40. package/src/components/ViewPackagePage/components/sidebar-packages-section.tsx +16 -0
  41. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +44 -0
  42. package/src/components/ViewPackagePage/components/sidebar.tsx +40 -0
  43. package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +9 -0
  44. package/src/components/ViewPackagePage/components/tab-views/bom-view.tsx +9 -0
  45. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +166 -0
  46. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +9 -0
  47. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +9 -0
  48. package/src/components/ViewPackagePage/components/theme-toggle.tsx +42 -0
  49. package/src/components/ViewPackagePage/hooks/use-mobile.tsx +19 -0
  50. package/src/components/ViewPackagePage/hooks/use-toast.ts +191 -0
  51. package/src/components/ViewPackagePage/simulate-page.tsx +120 -0
  52. package/src/components/ViewPackagePage/utils/is-package-file-important.ts +21 -0
  53. package/src/hooks/use-package-files.ts +1 -1
  54. package/src/index.css +15 -0
  55. package/src/pages/view-package.tsx +38 -0
@@ -0,0 +1,213 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect, useMemo } from "react"
4
+ import MainContentHeader from "./main-content-header"
5
+ import Sidebar from "./sidebar"
6
+ import MobileSidebar from "./mobile-sidebar"
7
+ import ImportantFilesView from "./important-files-view"
8
+ import { ShikiCodeViewer } from "./ShikiCodeViewer"
9
+
10
+ // Tab Views
11
+ import FilesView from "./tab-views/files-view"
12
+ import ThreeDView from "./tab-views/3d-view"
13
+ import PCBView from "./tab-views/pcb-view"
14
+ import SchematicView from "./tab-views/schematic-view"
15
+ import BOMView from "./tab-views/bom-view"
16
+ import { isPackageFileImportant } from "../utils/is-package-file-important"
17
+ import Header from "@/components/Header"
18
+ import Footer from "@/components/Footer"
19
+ import ViewSnippetHeader from "@/components/ViewSnippetHeader"
20
+ import PackageHeader from "./package-header"
21
+
22
+ interface PackageFile {
23
+ package_file_id: string
24
+ package_release_id: string
25
+ file_path: string
26
+ content_text: string
27
+ created_at: string // iso-8601
28
+ }
29
+
30
+ interface PackageInfo {
31
+ name: string
32
+ unscoped_name: string
33
+ owner_github_username: string
34
+ star_count: string
35
+ description: string
36
+ ai_description: string
37
+ ai_usage_instructions: string
38
+ creator_account_id?: string
39
+ owner_org_id?: string
40
+ }
41
+
42
+ interface RepoPageContentProps {
43
+ packageFiles?: PackageFile[]
44
+ importantFilePaths?: string[]
45
+ packageInfo?: PackageInfo
46
+ onFileClicked?: (file: any) => void
47
+ onDirectoryClicked?: (directory: any) => void
48
+ onExportClicked?: (exportType: string) => void
49
+ onEditClicked?: () => void
50
+ }
51
+
52
+ export default function RepoPageContent({
53
+ packageFiles,
54
+ packageInfo,
55
+ onFileClicked,
56
+ onDirectoryClicked,
57
+ onExportClicked,
58
+ onEditClicked,
59
+ }: RepoPageContentProps) {
60
+ const [activeView, setActiveView] = useState("files")
61
+
62
+ const importantFilePaths = packageFiles
63
+ ?.filter((pf) => isPackageFileImportant(pf.file_path))
64
+ ?.map((pf) => pf.file_path)
65
+
66
+ // Parse package files to determine directories and files structure
67
+ const { directories, files } = useMemo(() => {
68
+ if (!packageFiles) {
69
+ return { directories: [], files: [] }
70
+ }
71
+
72
+ const dirs = new Set<string>()
73
+ const filesList: Array<{
74
+ type: "file"
75
+ path: string
76
+ name: string
77
+ created_at: string
78
+ }> = []
79
+
80
+ packageFiles.forEach((file) => {
81
+ // Extract directory path
82
+ const pathParts = file.file_path.split("/")
83
+ const fileName = pathParts.pop() || ""
84
+
85
+ // Add all parent directories
86
+ let currentPath = ""
87
+ pathParts.forEach((part) => {
88
+ currentPath += (currentPath ? "/" : "") + part
89
+ dirs.add(currentPath)
90
+ })
91
+
92
+ filesList.push({
93
+ type: "file",
94
+ path: file.file_path,
95
+ name: fileName,
96
+ created_at: file.created_at,
97
+ })
98
+ })
99
+
100
+ // Convert directories set to array of directory objects
101
+ const dirsList = Array.from(dirs)
102
+ .map((path) => {
103
+ const pathParts = path.split("/")
104
+ if (!path) return null
105
+ return {
106
+ type: "directory",
107
+ path,
108
+ name: pathParts[pathParts.length - 1],
109
+ }
110
+ })
111
+ .filter((dir) => dir !== null)
112
+
113
+ return {
114
+ directories: dirsList,
115
+ files: filesList,
116
+ }
117
+ }, [packageFiles])
118
+
119
+ // Find important files based on importantFilePaths
120
+ const importantFiles = useMemo(() => {
121
+ if (!packageFiles || !importantFilePaths) return []
122
+
123
+ return packageFiles.filter((file) =>
124
+ importantFilePaths.some((path) => file.file_path.endsWith(path)),
125
+ )
126
+ }, [packageFiles, importantFilePaths])
127
+
128
+ // Render the appropriate content based on the active view
129
+ const renderContent = () => {
130
+ switch (activeView) {
131
+ case "files":
132
+ return (
133
+ <FilesView
134
+ directories={directories as any}
135
+ files={files as any}
136
+ isLoading={!packageFiles}
137
+ onFileClicked={onFileClicked}
138
+ onDirectoryClicked={onDirectoryClicked}
139
+ />
140
+ )
141
+ case "3d":
142
+ return <ThreeDView />
143
+ case "pcb":
144
+ return <PCBView />
145
+ case "schematic":
146
+ return <SchematicView />
147
+ case "bom":
148
+ return <BOMView />
149
+ default:
150
+ return (
151
+ <FilesView
152
+ directories={directories as any}
153
+ files={files as any}
154
+ isLoading={!packageFiles}
155
+ onFileClicked={onFileClicked}
156
+ onDirectoryClicked={onDirectoryClicked}
157
+ />
158
+ )
159
+ }
160
+ }
161
+
162
+ return (
163
+ <div className="min-h-screen bg-white dark:bg-[#0d1117] text-gray-900 dark:text-[#c9d1d9] font-sans">
164
+ <Header />
165
+ <PackageHeader packageInfo={packageInfo} />
166
+
167
+ {/* Mobile Sidebar */}
168
+ <div className="max-w-[1200px] mx-auto">
169
+ <MobileSidebar packageInfo={packageInfo} isLoading={!packageInfo} />
170
+ </div>
171
+
172
+ {/* Main Content */}
173
+ <div className="max-w-[1200px] mx-auto">
174
+ <div className="flex flex-col md:flex-row">
175
+ {/* Main Content Area */}
176
+ <div className="w-full md:flex-1 border-r border-gray-200 dark:border-[#30363d] p-4 md:max-w-[calc(100%-296px)] max-w-full">
177
+ {/* Main Content Header with Tabs */}
178
+ <MainContentHeader
179
+ activeView={activeView}
180
+ onViewChange={setActiveView}
181
+ onExportClicked={onExportClicked}
182
+ packageInfo={packageInfo}
183
+ />
184
+
185
+ {/* Dynamic Content based on active view */}
186
+ {renderContent()}
187
+
188
+ {/* Important Files View - Always shown */}
189
+ <ImportantFilesView
190
+ importantFiles={importantFiles}
191
+ isLoading={!packageFiles}
192
+ onEditClicked={onEditClicked}
193
+ aiDescription={packageInfo?.ai_description}
194
+ aiUsageInstructions={packageInfo?.ai_usage_instructions}
195
+ />
196
+ </div>
197
+
198
+ {/* Sidebar - Hidden on mobile, shown on md and up */}
199
+ <div className="hidden md:block md:w-[296px] flex-shrink-0">
200
+ <Sidebar
201
+ packageInfo={packageInfo}
202
+ isLoading={!packageInfo}
203
+ onViewChange={(view) => {
204
+ setActiveView(view)
205
+ }}
206
+ />
207
+ </div>
208
+ </div>
209
+ </div>
210
+ <Footer />
211
+ </div>
212
+ )
213
+ }
@@ -0,0 +1,12 @@
1
+ interface RepoTabHeaderProps {
2
+ activeTab: string
3
+ setActiveTab: (tab: string) => void
4
+ }
5
+
6
+ export default function RepoTabHeader({
7
+ activeTab,
8
+ setActiveTab,
9
+ }: RepoTabHeaderProps) {
10
+ // This component is being handled by a different team
11
+ return null
12
+ }
@@ -0,0 +1,103 @@
1
+ import { Badge } from "@/components/ui/badge"
2
+ import { GitFork, Star } from "lucide-react"
3
+ import { Skeleton } from "@/components/ui/skeleton"
4
+
5
+ interface PackageInfo {
6
+ name: string
7
+ unscoped_name: string
8
+ owner_github_username: string
9
+ star_count: string
10
+ description: string
11
+ ai_description: string
12
+ creator_account_id?: string
13
+ owner_org_id?: string
14
+ is_package?: boolean
15
+ website?: string
16
+ }
17
+
18
+ interface SidebarAboutSectionProps {
19
+ packageInfo?: PackageInfo
20
+ isLoading?: boolean
21
+ }
22
+
23
+ const LinkIcon = () => (
24
+ <svg className="h-4 w-4 mr-1" viewBox="0 0 16 16" fill="currentColor">
25
+ <path d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25Zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0Z"></path>
26
+ </svg>
27
+ )
28
+
29
+ export default function SidebarAboutSection({
30
+ packageInfo,
31
+ isLoading = false,
32
+ }: SidebarAboutSectionProps) {
33
+ const topics = packageInfo?.is_package ? ["Package"] : ["Board"]
34
+
35
+ if (isLoading) {
36
+ return (
37
+ <div className="mb-6">
38
+ <h2 className="text-lg font-semibold mb-2">About</h2>
39
+ <Skeleton className="h-4 w-full mb-3" />
40
+ <Skeleton className="h-4 w-3/4 mb-4" />
41
+ <div className="flex flex-wrap gap-2 mb-4">
42
+ {[1, 2, 3].map((i) => (
43
+ <Skeleton key={i} className="h-6 w-20 rounded-full" />
44
+ ))}
45
+ </div>
46
+ <div className="space-y-2">
47
+ <Skeleton className="h-4 w-24" />
48
+ <Skeleton className="h-4 w-20" />
49
+ <Skeleton className="h-4 w-20" />
50
+ </div>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ return (
56
+ <div className="mb-6">
57
+ <h2 className="text-lg font-semibold mb-2">About</h2>
58
+ <p className="text-sm mb-3">
59
+ {packageInfo?.description || packageInfo?.ai_description}
60
+ </p>
61
+ {packageInfo?.website && (
62
+ <a
63
+ href="#"
64
+ className="text-blue-600 dark:text-[#58a6ff] hover:underline text-sm flex items-center mb-4"
65
+ >
66
+ <LinkIcon />
67
+ {packageInfo?.website}
68
+ </a>
69
+ )}
70
+ <div className="flex flex-wrap gap-2 mb-4">
71
+ {topics.map((topic, index) => (
72
+ <Badge
73
+ key={index}
74
+ variant="outline"
75
+ className="text-xs rounded-full px-2 py-0.5 bg-blue-100 dark:bg-[#1f6feb33] text-blue-600 dark:text-[#58a6ff] border-blue-300 dark:border-[#1f6feb66]"
76
+ >
77
+ {topic}
78
+ </Badge>
79
+ ))}
80
+ </div>
81
+ <div className="space-y-2 text-sm">
82
+ <div className="flex items-center">
83
+ <svg
84
+ className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]"
85
+ viewBox="0 0 16 16"
86
+ fill="currentColor"
87
+ >
88
+ <path d="M8.75.75V2h.985c.304 0 .603.08.867.231l1.29.736c.038.022.08.033.124.033h2.234a.75.75 0 0 1 0 1.5h-.427l2.111 4.692a.75.75 0 0 1-.154.838l-.53-.53.53.53-.001.002-.002.002-.006.006-.006.005-.01.01-.045.04c-.21.176-.441.327-.686.45C14.556 10.78 13.88 11 13 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L12.178 4.5h-.162c-.305 0-.604-.079-.868-.231l-1.29-.736a.245.245 0 0 0-.124-.033H8.75V13h2.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5h2.5V3.5h-.984a.245.245 0 0 0-.124.033l-1.29.736c-.264.152-.563.231-.868.231h-.162l2.112 4.692a.75.75 0 0 1-.154.838l-.53-.53.53.53-.001.002-.002.002-.006.006-.016.015-.045.04c-.21.176-.441.327-.686.45C4.556 10.78 3.88 11 3 11a4.498 4.498 0 0 1-2.023-.454 3.544 3.544 0 0 1-.686-.45l-.045-.04-.016-.015-.006-.006-.004-.004v-.001a.75.75 0 0 1-.154-.838L2.178 4.5H1.75a.75.75 0 0 1 0-1.5h2.234a.249.249 0 0 0 .125-.033l1.29-.736c.263-.15.561-.231.865-.231H7.25V.75a.75.75 0 0 1 1.5 0Zm2.945 8.477c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327Zm-10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327Z"></path>
89
+ </svg>
90
+ <span>MIT license</span>
91
+ </div>
92
+ <div className="flex items-center">
93
+ <Star className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
94
+ <span>{packageInfo?.star_count || "16"} stars</span>
95
+ </div>
96
+ <div className="flex items-center">
97
+ <GitFork className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
98
+ <span>39 forks</span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,31 @@
1
+ import { Badge } from "@/components/ui/badge"
2
+
3
+ export default function SidebarContributorsSection() {
4
+ return (
5
+ <div>
6
+ <div className="flex items-center justify-between mb-2">
7
+ <h2 className="text-lg font-semibold">Contributors</h2>
8
+ <Badge
9
+ variant="outline"
10
+ className="text-xs rounded-full px-2 py-0.5 bg-transparent border-gray-300 dark:border-[#30363d]"
11
+ >
12
+ 18
13
+ </Badge>
14
+ </div>
15
+ <div className="flex flex-wrap gap-1 mb-2">
16
+ {[...Array(10)].map((_, i) => (
17
+ <div
18
+ key={i}
19
+ className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500"
20
+ ></div>
21
+ ))}
22
+ </div>
23
+ <a
24
+ href="#"
25
+ className="text-blue-600 dark:text-[#58a6ff] hover:underline text-sm"
26
+ >
27
+ + 4 contributors
28
+ </a>
29
+ </div>
30
+ )
31
+ }
@@ -0,0 +1,16 @@
1
+ export default function SidebarPackagesSection() {
2
+ return (
3
+ <div className="mb-6">
4
+ <h2 className="text-lg font-semibold mb-2">Packages</h2>
5
+ <p className="text-sm text-gray-500 dark:text-[#8b949e] mb-1">
6
+ No packages published
7
+ </p>
8
+ <a
9
+ href="#"
10
+ className="text-blue-600 dark:text-[#58a6ff] hover:underline text-sm"
11
+ >
12
+ Publish your first package
13
+ </a>
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,44 @@
1
+ import { Tag, Clock } from "lucide-react"
2
+ import { Skeleton } from "@/components/ui/skeleton"
3
+
4
+ interface SidebarReleasesSectionProps {
5
+ isLoading?: boolean
6
+ }
7
+
8
+ export default function SidebarReleasesSection({
9
+ isLoading = false,
10
+ }: SidebarReleasesSectionProps) {
11
+ if (isLoading) {
12
+ return (
13
+ <div className="mb-6">
14
+ <h2 className="text-lg font-semibold mb-2">Releases</h2>
15
+ <div className="mb-2">
16
+ <Skeleton className="h-4 w-20" />
17
+ </div>
18
+ <div className="mb-3">
19
+ <Skeleton className="h-4 w-24" />
20
+ </div>
21
+ <Skeleton className="h-4 w-32" />
22
+ </div>
23
+ )
24
+ }
25
+
26
+ return (
27
+ <div className="mb-6">
28
+ <h2 className="text-lg font-semibold mb-2">Releases</h2>
29
+ <div className="flex items-center mb-2">
30
+ <Tag className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
31
+ <span className="text-sm font-medium">v0.0.361</span>
32
+ </div>
33
+ <div className="flex items-center mb-3">
34
+ <Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
35
+ <span className="text-sm text-gray-500 dark:text-[#8b949e]">
36
+ 2 days ago
37
+ </span>
38
+ </div>
39
+ {/* <a href="#" className="text-blue-600 dark:text-[#58a6ff] hover:underline text-sm">
40
+ Push a new release
41
+ </a> */}
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,40 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import SidebarAboutSection from "./sidebar-about-section"
5
+ import SidebarReleasesSection from "./sidebar-releases-section"
6
+ import PreviewImageSquares from "./preview-image-squares"
7
+
8
+ interface PackageInfo {
9
+ name: string
10
+ unscoped_name: string
11
+ owner_github_username: string
12
+ star_count: string
13
+ description: string
14
+ ai_description: string
15
+ creator_account_id?: string
16
+ owner_org_id?: string
17
+ }
18
+
19
+ interface SidebarProps {
20
+ packageInfo?: PackageInfo
21
+ isLoading?: boolean
22
+ onViewChange?: (view: "3d" | "pcb" | "schematic") => void
23
+ }
24
+
25
+ export default function Sidebar({
26
+ packageInfo,
27
+ isLoading = false,
28
+ onViewChange,
29
+ }: SidebarProps) {
30
+ return (
31
+ <div className="h-full p-4 bg-white dark:bg-[#0d1117] overflow-y-auto">
32
+ <SidebarAboutSection packageInfo={packageInfo} isLoading={isLoading} />
33
+ <PreviewImageSquares
34
+ packageInfo={packageInfo}
35
+ onViewChange={onViewChange}
36
+ />
37
+ <SidebarReleasesSection isLoading={isLoading} />
38
+ </div>
39
+ )
40
+ }
@@ -0,0 +1,9 @@
1
+ export default function ThreeDView() {
2
+ return (
3
+ <div className="border border-gray-200 dark:border-[#30363d] rounded-md p-8 mb-4 bg-white dark:bg-[#0d1117] flex items-center justify-center">
4
+ <h2 className="text-xl font-semibold text-gray-500 dark:text-[#8b949e]">
5
+ 3D View Tab
6
+ </h2>
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,9 @@
1
+ export default function BOMView() {
2
+ return (
3
+ <div className="border border-gray-200 dark:border-[#30363d] rounded-md p-8 mb-4 bg-white dark:bg-[#0d1117] flex items-center justify-center">
4
+ <h2 className="text-xl font-semibold text-gray-500 dark:text-[#8b949e]">
5
+ BOM View Tab
6
+ </h2>
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,166 @@
1
+ "use client"
2
+
3
+ import { FileText, Folder } from "lucide-react"
4
+ import { Skeleton } from "@/components/ui/skeleton"
5
+
6
+ interface Directory {
7
+ type: "directory"
8
+ path: string
9
+ name: string
10
+ }
11
+
12
+ interface File {
13
+ type: "file"
14
+ path: string
15
+ name: string
16
+ content: string
17
+ created_at: string
18
+ }
19
+
20
+ interface FilesViewProps {
21
+ directories?: Directory[]
22
+ files?: File[]
23
+ isLoading?: boolean
24
+ onFileClicked?: (file: File) => void
25
+ onDirectoryClicked?: (directory: Directory) => void
26
+ }
27
+
28
+ export default function FilesView({
29
+ directories = [],
30
+ files = [],
31
+ isLoading = false,
32
+ onFileClicked,
33
+ onDirectoryClicked,
34
+ }: FilesViewProps) {
35
+ // Format date for display
36
+ const formatDate = (dateString: string) => {
37
+ const date = new Date(dateString)
38
+ const now = new Date()
39
+ const diffTime = Math.abs(now.getTime() - date.getTime())
40
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
41
+
42
+ if (diffDays < 1) return "today"
43
+ if (diffDays === 1) return "yesterday"
44
+ if (diffDays < 7) return `${diffDays} days ago`
45
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
46
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
47
+ return `${Math.floor(diffDays / 365)} years ago`
48
+ }
49
+
50
+ // Combine directories and files for display
51
+ const items = [
52
+ ...directories.map((dir) => ({
53
+ ...dir,
54
+ message: "", // TODO insert ai description of directory here!
55
+ time: "",
56
+ })),
57
+ ...files.map((file) => ({
58
+ ...file,
59
+ message: "", // TODO insert ai description of file here!
60
+ time: formatDate(file.created_at),
61
+ })),
62
+ ].sort((a, b) => {
63
+ // Sort directories first, then files
64
+ if (a.type === "directory" && b.type === "file") return -1
65
+ if (a.type === "file" && b.type === "directory") return 1
66
+ // Then sort alphabetically by name
67
+ return a.name.localeCompare(b.name)
68
+ })
69
+
70
+ const handleItemClick = (item: any) => {
71
+ if (item.type === "directory" && onDirectoryClicked) {
72
+ onDirectoryClicked(item)
73
+ } else if (item.type === "file" && onFileClicked) {
74
+ onFileClicked(item)
75
+ }
76
+ }
77
+
78
+ if (isLoading) {
79
+ return (
80
+ <div className="mb-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
81
+ <div className="flex items-center px-4 py-2 md:py-3 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
82
+ <Skeleton className="h-4 w-24" />
83
+ <div className="ml-auto flex items-center space-x-4">
84
+ <Skeleton className="h-4 w-20" />
85
+ <Skeleton className="h-4 w-24" />
86
+ </div>
87
+ </div>
88
+ <div className="bg-white dark:bg-[#0d1117]">
89
+ {[...Array(5)].map((_, index) => (
90
+ <div
91
+ key={index}
92
+ className="flex items-center px-4 py-2 border-b border-gray-200 dark:border-[#30363d]"
93
+ >
94
+ <Skeleton className="h-4 w-4 mr-2" />
95
+ <Skeleton className="h-4 w-32" />
96
+ <div className="ml-auto flex items-center space-x-4">
97
+ <Skeleton className="h-4 w-40" />
98
+ <Skeleton className="h-4 w-16" />
99
+ </div>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ return (
108
+ <div className="mb-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
109
+ <div className="flex items-center px-4 py-2 md:py-3 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
110
+ {/* Desktop view */}
111
+ <div className="hidden md:flex items-center text-xs">
112
+ <span className="text-gray-500 dark:text-[#8b949e]">Files</span>
113
+ </div>
114
+ <div className="hidden md:flex ml-auto items-center text-xs text-gray-500 dark:text-[#8b949e]">
115
+ <span>
116
+ {files.length} files, {directories.length} directories
117
+ </span>
118
+ </div>
119
+
120
+ {/* Mobile view */}
121
+ <div className="md:hidden flex items-center justify-between w-full">
122
+ <div className="flex items-center">
123
+ <span className="text-xs text-gray-500 dark:text-[#8b949e]">
124
+ Files
125
+ </span>
126
+ </div>
127
+ <div className="flex items-center text-xs text-gray-500 dark:text-[#8b949e]">
128
+ <span>{files.length + directories.length} items</span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Files and Directories */}
134
+ <div className="bg-white dark:bg-[#0d1117]">
135
+ {items.length === 0 ? (
136
+ <div className="px-4 py-8 text-center text-gray-500 dark:text-[#8b949e]">
137
+ No files found
138
+ </div>
139
+ ) : (
140
+ items.map((item, index) => (
141
+ <div
142
+ key={index}
143
+ className="flex items-center px-4 py-2 hover:bg-gray-50 dark:hover:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d] cursor-pointer group"
144
+ onClick={() => handleItemClick(item)}
145
+ >
146
+ {item.type === "directory" ? (
147
+ <Folder className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
148
+ ) : (
149
+ <FileText className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
150
+ )}
151
+ <span className="text-sm group-hover:underline">{item.name}</span>
152
+ <span className="ml-auto text-xs text-gray-500 dark:text-[#8b949e]">
153
+ {item.message}
154
+ </span>
155
+ {item.time && (
156
+ <span className="ml-4 text-xs text-gray-500 dark:text-[#8b949e]">
157
+ {item.time}
158
+ </span>
159
+ )}
160
+ </div>
161
+ ))
162
+ )}
163
+ </div>
164
+ </div>
165
+ )
166
+ }
@@ -0,0 +1,9 @@
1
+ export default function PCBView() {
2
+ return (
3
+ <div className="border border-gray-200 dark:border-[#30363d] rounded-md p-8 mb-4 bg-white dark:bg-[#0d1117] flex items-center justify-center">
4
+ <h2 className="text-xl font-semibold text-gray-500 dark:text-[#8b949e]">
5
+ PCB View Tab
6
+ </h2>
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,9 @@
1
+ export default function SchematicView() {
2
+ return (
3
+ <div className="border border-gray-200 dark:border-[#30363d] rounded-md p-8 mb-4 bg-white dark:bg-[#0d1117] flex items-center justify-center">
4
+ <h2 className="text-xl font-semibold text-gray-500 dark:text-[#8b949e]">
5
+ Schematic View Tab
6
+ </h2>
7
+ </div>
8
+ )
9
+ }