@tscircuit/fake-snippets 0.0.34 → 0.0.35

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 (42) hide show
  1. package/.github/workflows/bun-test.yml +3 -0
  2. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +12 -1
  3. package/bun-tests/fake-snippets-api/routes/accounts/get_account_balance.test.ts +1 -5
  4. package/bun-tests/fake-snippets-api/routes/orders/create.test.ts +0 -1
  5. package/bun-tests/fake-snippets-api/routes/orders/list.test.ts +3 -3
  6. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +28 -84
  7. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +12 -36
  8. package/bun-tests/fake-snippets-api/routes/package_releases/list.test.ts +4 -12
  9. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +16 -48
  10. package/bun-tests/fake-snippets-api/routes/packages/add_star.test.ts +12 -44
  11. package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +4 -12
  12. package/bun-tests/fake-snippets-api/routes/packages/delete.test.ts +15 -41
  13. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +1 -5
  14. package/bun-tests/fake-snippets-api/routes/packages/list-2.test.ts +6 -19
  15. package/bun-tests/fake-snippets-api/routes/packages/remove_star.test.ts +9 -36
  16. package/bun-tests/fake-snippets-api/routes/packages/update.test.ts +26 -74
  17. package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +12 -47
  18. package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +6 -14
  19. package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +9 -33
  20. package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +3 -12
  21. package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +9 -34
  22. package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +12 -44
  23. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +12 -36
  24. package/bun.lock +27 -29
  25. package/dist/bundle.js +45 -27
  26. package/fake-snippets-api/lib/middleware/with-optional-session-auth.ts +27 -8
  27. package/fake-snippets-api/lib/middleware/with-session-auth.ts +17 -6
  28. package/fake-snippets-api/routes/api/snippets/add_star.ts +2 -4
  29. package/fake-snippets-api/routes/api/snippets/create.ts +8 -10
  30. package/package.json +5 -4
  31. package/src/App.tsx +3 -0
  32. package/src/components/CodeAndPreview.tsx +2 -2
  33. package/src/components/CodeEditor.tsx +1 -1
  34. package/src/components/DownloadButtonAndMenu.tsx +13 -11
  35. package/src/components/Footer.tsx +3 -0
  36. package/src/components/SnippetCard.tsx +159 -0
  37. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +89 -27
  38. package/src/pages/404.tsx +56 -0
  39. package/src/pages/trending.tsx +222 -0
  40. package/src/pages/user-profile.tsx +11 -102
  41. package/src/pages/view-package.tsx +18 -2
  42. package/src/lib/templates/manual-edits-template.ts +0 -5
@@ -0,0 +1,222 @@
1
+ import React, { useEffect, useState } from "react"
2
+ import { useQuery } from "react-query"
3
+ import { useAxios } from "@/hooks/use-axios"
4
+ import { Snippet } from "fake-snippets-api/lib/db/schema"
5
+ import Header from "@/components/Header"
6
+ import Footer from "@/components/Footer"
7
+ import { Link } from "wouter"
8
+ import { StarIcon, LockClosedIcon } from "@radix-ui/react-icons"
9
+ import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
10
+ import { OptimizedImage } from "@/components/OptimizedImage"
11
+ import {
12
+ GlobeIcon,
13
+ PencilIcon,
14
+ Zap,
15
+ Tag,
16
+ Calendar,
17
+ Search,
18
+ Keyboard,
19
+ Cpu,
20
+ Layers,
21
+ LucideBellElectric,
22
+ } from "lucide-react"
23
+ import { Input } from "@/components/ui/input"
24
+ import { Badge } from "@/components/ui/badge"
25
+ import { SnippetTypeIcon } from "@/components/SnippetTypeIcon"
26
+ import { timeAgo } from "@/lib/utils/timeAgo"
27
+ import {
28
+ Select,
29
+ SelectContent,
30
+ SelectItem,
31
+ SelectTrigger,
32
+ SelectValue,
33
+ } from "@/components/ui/select"
34
+ import { SnippetCard } from "@/components/SnippetCard"
35
+
36
+ const TrendingPage: React.FC = () => {
37
+ const axios = useAxios()
38
+ const apiBaseUrl = useSnippetsBaseApiUrl()
39
+ const [searchQuery, setSearchQuery] = useState("")
40
+ const [category, setCategory] = useState("all")
41
+
42
+ const {
43
+ data: snippets,
44
+ isLoading,
45
+ error,
46
+ refetch,
47
+ } = useQuery<Snippet[]>(
48
+ ["trendingSnippets", category],
49
+ async () => {
50
+ const params = category !== "all" ? { tag: category } : {}
51
+ const response = await axios.get("/snippets/list_trending", { params })
52
+ return response.data.snippets
53
+ },
54
+ {
55
+ keepPreviousData: true,
56
+ },
57
+ )
58
+
59
+ const filteredSnippets = snippets?.filter((snippet) => {
60
+ if (!searchQuery) return true
61
+
62
+ const query = searchQuery.toLowerCase().trim()
63
+
64
+ const searchableFields = [
65
+ snippet.unscoped_name.toLowerCase(),
66
+ snippet.owner_name.toLowerCase(),
67
+ (snippet.description || "").toLowerCase(),
68
+ ]
69
+
70
+ return searchableFields.some((field) => {
71
+ const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
72
+ return queryWords.every((word) => field.includes(word))
73
+ })
74
+ })
75
+
76
+ return (
77
+ <div className="min-h-screen flex flex-col bg-gray-50">
78
+ <Header />
79
+ <main className="flex-grow container mx-auto px-4 py-8">
80
+ <div className="mb-8 max-w-3xl">
81
+ <div className="flex items-center gap-2 mb-3">
82
+ <Zap className="w-6 h-6 text-amber-500" />
83
+ <h1 className="text-4xl font-bold text-gray-900">
84
+ Trending Snippets
85
+ </h1>
86
+ </div>
87
+ <p className="text-lg text-gray-600 mb-4">
88
+ Discover the most popular and innovative snippets from the community
89
+ over the last 7 days. These trending designs showcase the best in
90
+ circuit creativity and technical excellence.
91
+ </p>
92
+ <div className="flex flex-wrap gap-3">
93
+ <Badge variant="secondary" className="px-3 py-1">
94
+ <Tag className="w-3.5 h-3.5 mr-1" />
95
+ <span>Most Starred</span>
96
+ </Badge>
97
+ <Badge variant="secondary" className="px-3 py-1">
98
+ <Calendar className="w-3.5 h-3.5 mr-1" />
99
+ <span>Last 7 Days</span>
100
+ </Badge>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="mb-6">
105
+ <div className="flex flex-col sm:flex-row gap-4 mb-4">
106
+ <div className="relative flex-grow">
107
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
108
+ <Input
109
+ type="text"
110
+ placeholder="Search trending snippets..."
111
+ value={searchQuery}
112
+ onChange={(e) => setSearchQuery(e.target.value)}
113
+ className="pl-10"
114
+ />
115
+ </div>
116
+ <Select value={category} onValueChange={setCategory}>
117
+ <SelectTrigger className="w-full sm:w-[180px]">
118
+ <SelectValue placeholder="Category" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ <SelectItem value="all">All Categories</SelectItem>
122
+ <SelectItem value="keyboard">
123
+ <div className="flex items-center">
124
+ <Keyboard className="mr-2 h-4 w-4" />
125
+ <span>Keyboards</span>
126
+ </div>
127
+ </SelectItem>
128
+ <SelectItem value="microcontroller">
129
+ <div className="flex items-center">
130
+ <Cpu className="mr-2 h-4 w-4" />
131
+ <span>Microcontrollers</span>
132
+ </div>
133
+ </SelectItem>
134
+ <SelectItem value="connector">
135
+ <div className="flex items-center">
136
+ <Layers className="mr-2 h-4 w-4" />
137
+ <span>Connectors</span>
138
+ </div>
139
+ </SelectItem>
140
+ <SelectItem value="sensor">
141
+ <div className="flex items-center">
142
+ <LucideBellElectric className="mr-2 h-4 w-4" />
143
+ <span>Sensors</span>
144
+ </div>
145
+ </SelectItem>
146
+ </SelectContent>
147
+ </Select>
148
+ </div>
149
+ </div>
150
+
151
+ {isLoading ? (
152
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
153
+ {[...Array(6)].map((_, i) => (
154
+ <div key={i} className="border p-4 rounded-md animate-pulse">
155
+ <div className="flex items-start gap-4">
156
+ <div className="h-16 w-16 flex-shrink-0 rounded-md bg-slate-200"></div>
157
+ <div className="flex-1">
158
+ <div className="h-5 bg-slate-200 rounded w-3/4 mb-2"></div>
159
+ <div className="h-4 bg-slate-200 rounded w-1/2 mb-2"></div>
160
+ <div className="flex gap-2">
161
+ <div className="h-3 bg-slate-200 rounded w-16"></div>
162
+ <div className="h-3 bg-slate-200 rounded w-16"></div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ ) : error ? (
170
+ <div className="bg-red-50 border border-red-200 text-red-700 p-6 rounded-xl shadow-sm max-w-2xl mx-auto">
171
+ <div className="flex items-start">
172
+ <div className="mr-4 bg-red-100 p-2 rounded-full">
173
+ <Search className="w-6 h-6 text-red-600" />
174
+ </div>
175
+ <div>
176
+ <h3 className="text-lg font-semibold mb-2">
177
+ Error Loading Snippets
178
+ </h3>
179
+ <p className="text-red-600">
180
+ We couldn't load the trending snippets. Please try again
181
+ later.
182
+ </p>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ ) : filteredSnippets?.length === 0 ? (
187
+ <div className="text-center py-12 px-4">
188
+ <div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
189
+ <Search className="w-8 h-8 text-slate-400" />
190
+ </div>
191
+ <h3 className="text-xl font-medium text-slate-900 mb-2">
192
+ No Matching Snippets
193
+ </h3>
194
+ <p className="text-slate-500 max-w-md mx-auto mb-6">
195
+ {searchQuery
196
+ ? `No snippets match your search for "${searchQuery}".`
197
+ : category !== "all"
198
+ ? `No ${category} snippets found in the trending list.`
199
+ : "There are no trending snippets at the moment."}
200
+ </p>
201
+ </div>
202
+ ) : (
203
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
204
+ {filteredSnippets
205
+ ?.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
206
+ ?.map((snippet) => (
207
+ <SnippetCard
208
+ key={snippet.snippet_id}
209
+ snippet={snippet}
210
+ baseUrl={apiBaseUrl}
211
+ showOwner={true}
212
+ />
213
+ ))}
214
+ </div>
215
+ )}
216
+ </main>
217
+ <Footer />
218
+ </div>
219
+ )
220
+ }
221
+
222
+ export default TrendingPage
@@ -5,25 +5,15 @@ import { useAxios } from "@/hooks/use-axios"
5
5
  import Header from "@/components/Header"
6
6
  import Footer from "@/components/Footer"
7
7
  import { Snippet } from "fake-snippets-api/lib/db/schema"
8
- import { Link } from "wouter"
9
8
  import { Button } from "@/components/ui/button"
10
- import { GitHubLogoIcon, StarIcon, LockClosedIcon } from "@radix-ui/react-icons"
9
+ import { GitHubLogoIcon } from "@radix-ui/react-icons"
11
10
  import { Input } from "@/components/ui/input"
12
11
  import { useGlobalStore } from "@/hooks/use-global-store"
13
- import { GlobeIcon, MoreVertical, PencilIcon, Trash2 } from "lucide-react"
14
12
  import { useConfirmDeleteSnippetDialog } from "@/components/dialogs/confirm-delete-snippet-dialog"
15
13
  import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
16
- import {
17
- DropdownMenu,
18
- DropdownMenuContent,
19
- DropdownMenuItem,
20
- DropdownMenuTrigger,
21
- } from "@/components/ui/dropdown-menu"
22
14
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
23
- import { OptimizedImage } from "@/components/OptimizedImage"
24
15
  import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
25
- import { SnippetTypeIcon } from "@/components/SnippetTypeIcon"
26
- import { timeAgo } from "@/lib/utils/timeAgo"
16
+ import { SnippetCard } from "@/components/SnippetCard"
27
17
 
28
18
  export const UserProfilePage = () => {
29
19
  const { username } = useParams()
@@ -132,97 +122,16 @@ export const UserProfilePage = () => {
132
122
  {filteredSnippets
133
123
  ?.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
134
124
  ?.map((snippet) => (
135
- <Link
125
+ <SnippetCard
136
126
  key={snippet.snippet_id}
137
- href={`/${snippet.owner_name}/${snippet.unscoped_name}`}
138
- >
139
- <div className="border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4">
140
- <div className="flex items-start gap-4">
141
- <div className="h-16 w-16 flex-shrink-0 rounded-md overflow-hidden">
142
- <OptimizedImage
143
- src={`${baseUrl}/snippets/images/${snippet.owner_name}/${snippet.unscoped_name}/pcb.svg`}
144
- alt={`${snippet.owner_name}'s profile`}
145
- className="object-cover h-full w-full transition-transform duration-300 -rotate-45 hover:rotate-0 hover:scale-110 scale-150"
146
- />
147
- </div>
148
- <div className="flex-1 min-w-0">
149
- <div className="flex justify-between items-start mb-[2px] -mt-[3px]">
150
- <h2 className="text-md font-semibold truncate pr-[30px]">
151
- {activeTab === "starred" && (
152
- <>
153
- <span className="text-gray-700 text-md">
154
- {snippet.owner_name}
155
- </span>
156
- <span className="mx-1">/</span>
157
- </>
158
- )}
159
- <span className="text-gray-900">
160
- {snippet.unscoped_name}
161
- </span>
162
- </h2>
163
- <div className="flex items-center gap-2">
164
- <SnippetTypeIcon
165
- type={snippet.snippet_type}
166
- className="pt-[2.5px]"
167
- />
168
- <div className="flex items-center gap-1 text-gray-600">
169
- <StarIcon className="w-4 h-4 pt-[2.5px]" />
170
- <span className="text-[16px]">
171
- {snippet.star_count || 0}
172
- </span>
173
- </div>
174
- {isCurrentUserProfile && activeTab === "all" && (
175
- <DropdownMenu>
176
- <DropdownMenuTrigger asChild>
177
- <Button
178
- variant="ghost"
179
- size="icon"
180
- className="h-[1.5rem] w-[1.5rem]"
181
- >
182
- <MoreVertical className="h-4 w-4" />
183
- </Button>
184
- </DropdownMenuTrigger>
185
- <DropdownMenuContent>
186
- <DropdownMenuItem
187
- className="text-xs text-red-600"
188
- onClick={(e) =>
189
- handleDeleteClick(e, snippet)
190
- }
191
- >
192
- <Trash2 className="mr-2 h-3 w-3" />
193
- Delete Snippet
194
- </DropdownMenuItem>
195
- </DropdownMenuContent>
196
- </DropdownMenu>
197
- )}
198
- </div>
199
- </div>
200
- <p
201
- className={`${!snippet.description && "h-[1.25rem]"} text-sm text-gray-500 mb-1 truncate max-w-xs`}
202
- >
203
- {snippet.description ? snippet.description : " "}
204
- </p>
205
- <div className={`flex items-center gap-4`}>
206
- {snippet.is_private ? (
207
- <div className="flex items-center text-xs gap-1 text-gray-500">
208
- <LockClosedIcon height={12} width={12} />
209
- <span>Private</span>
210
- </div>
211
- ) : (
212
- <div className="flex items-center text-xs gap-1 text-gray-500">
213
- <GlobeIcon height={12} width={12} />
214
- <span>Public</span>
215
- </div>
216
- )}
217
- <div className="flex items-center text-xs gap-1 text-gray-500">
218
- <PencilIcon height={12} width={12} />
219
- <span>{timeAgo(new Date(snippet.updated_at))}</span>
220
- </div>
221
- </div>
222
- </div>
223
- </div>
224
- </div>
225
- </Link>
127
+ snippet={snippet}
128
+ baseUrl={baseUrl}
129
+ showOwner={activeTab === "starred"}
130
+ isCurrentUserSnippet={
131
+ isCurrentUserProfile && activeTab === "all"
132
+ }
133
+ onDeleteClick={handleDeleteClick}
134
+ />
226
135
  ))}
227
136
  </div>
228
137
  )}
@@ -1,17 +1,23 @@
1
1
  import RepoPageContent from "@/components/ViewPackagePage/components/repo-page-content"
2
2
  import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
3
- import { usePackageByName } from "@/hooks/use-package-by-package-name"
4
3
  import { usePackageFiles } from "@/hooks/use-package-files"
5
4
  import { usePackageRelease } from "@/hooks/use-package-release"
6
5
  import { useLocation, useParams } from "wouter"
7
6
  import { Helmet } from "react-helmet-async"
7
+ import { useEffect, useState } from "react"
8
+ import NotFoundPage from "./404"
8
9
 
9
10
  export const ViewPackagePage = () => {
10
11
  const { packageInfo } = useCurrentPackageInfo()
11
12
  const { author, packageName } = useParams()
12
13
  const [, setLocation] = useLocation()
14
+ const [isNotFound, setIsNotFound] = useState(false)
13
15
 
14
- const { data: packageRelease } = usePackageRelease({
16
+ const {
17
+ data: packageRelease,
18
+ error: packageReleaseError,
19
+ isLoading: isLoadingPackageRelease,
20
+ } = usePackageRelease({
15
21
  is_latest: true,
16
22
  package_name: `${author}/${packageName}`,
17
23
  })
@@ -19,6 +25,16 @@ export const ViewPackagePage = () => {
19
25
  const { data: packageFiles } = usePackageFiles(
20
26
  packageRelease?.package_release_id,
21
27
  )
28
+ useEffect(() => {
29
+ if (isLoadingPackageRelease) return
30
+ if (packageReleaseError?.status == 404) {
31
+ setIsNotFound(true)
32
+ }
33
+ }, [isLoadingPackageRelease, packageReleaseError])
34
+
35
+ if (isNotFound) {
36
+ return <NotFoundPage heading="Package Not Found" />
37
+ }
22
38
 
23
39
  return (
24
40
  <>
@@ -1,5 +0,0 @@
1
- export default {
2
- pcb_placements: [],
3
- edit_events: [],
4
- manual_trace_hints: [],
5
- }