@tscircuit/fake-snippets 0.0.37 → 0.0.39
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.
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +2 -2
- package/dist/bundle.js +8 -9
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/fake-snippets-api/lib/db/db-client.ts +1 -1
- package/fake-snippets-api/routes/api/packages/delete.ts +1 -3
- package/fake-snippets-api/routes/api/snippets/{list_newest.ts → list_latest.ts} +2 -2
- package/package.json +1 -1
- package/scripts/generate-sitemap.ts +1 -1
- package/src/App.tsx +2 -2
- package/src/components/Footer.tsx +2 -2
- package/src/components/HiddenFilesDropdown.tsx +44 -0
- package/src/components/LatestSnippets.tsx +1 -1
- package/src/components/ViewPackagePage/components/tab-views/3d-view.tsx +1 -1
- package/src/components/ViewPackagePage/components/tab-views/bom-view.tsx +1 -1
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +23 -2
- package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +1 -0
- package/src/pages/dashboard.tsx +8 -8
- package/src/pages/latest.tsx +212 -0
- package/src/pages/newest.tsx +0 -16
package/dist/index.d.ts
CHANGED
|
@@ -702,7 +702,7 @@ declare const createDatabase: ({ seed }?: {
|
|
|
702
702
|
step_function_name: string | null;
|
|
703
703
|
error_message: string | null;
|
|
704
704
|
}[];
|
|
705
|
-
}, "addOrder" | "getOrderById" | "getOrderFilesByOrderId" | "getJlcpcbOrderStatesByOrderId" | "getJlcpcbOrderStepRunsByJlcpcbOrderStateId" | "updateOrder" | "addJlcpcbOrderState" | "updateJlcpcbOrderState" | "addOrderFile" | "getOrderFileById" | "addAccount" | "addAccountPackage" | "getAccountPackageById" | "updateAccountPackage" | "deleteAccountPackage" | "addSnippet" | "
|
|
705
|
+
}, "addOrder" | "getOrderById" | "getOrderFilesByOrderId" | "getJlcpcbOrderStatesByOrderId" | "getJlcpcbOrderStepRunsByJlcpcbOrderStateId" | "updateOrder" | "addJlcpcbOrderState" | "updateJlcpcbOrderState" | "addOrderFile" | "getOrderFileById" | "addAccount" | "addAccountPackage" | "getAccountPackageById" | "updateAccountPackage" | "deleteAccountPackage" | "addSnippet" | "getLatestSnippets" | "getTrendingSnippets" | "getPackagesByAuthor" | "getSnippetByAuthorAndName" | "updateSnippet" | "getSnippetById" | "searchSnippets" | "deleteSnippet" | "addSession" | "getSessions" | "createLoginPage" | "getLoginPage" | "updateLoginPage" | "getAccount" | "updateAccount" | "createSession" | "addStar" | "removeStar" | "hasStarred" | "addPackage" | "updatePackage" | "getPackageById" | "getPackageReleaseById" | "addPackageRelease" | "updatePackageRelease" | "deletePackageFile" | "addPackageFile" | "updatePackageFile" | "getStarCount" | "getPackageFilesByReleaseId"> & {
|
|
706
706
|
addOrder: (order: Omit<Order, "order_id">) => Order;
|
|
707
707
|
getOrderById: (orderId: string) => Order | undefined;
|
|
708
708
|
getOrderFilesByOrderId: (orderId: string) => OrderFile[];
|
|
@@ -734,7 +734,7 @@ declare const createDatabase: ({ seed }?: {
|
|
|
734
734
|
updateAccountPackage: (accountPackageId: string, updates: Partial<AccountPackage>) => void;
|
|
735
735
|
deleteAccountPackage: (accountPackageId: string) => boolean;
|
|
736
736
|
addSnippet: (snippet: Omit<z.input<typeof snippetSchema>, "snippet_id" | "package_release_id">) => Snippet;
|
|
737
|
-
|
|
737
|
+
getLatestSnippets: (limit: number) => Snippet[];
|
|
738
738
|
getTrendingSnippets: (limit: number, since: string) => Snippet[];
|
|
739
739
|
getPackagesByAuthor: (authorName?: string) => Package[];
|
|
740
740
|
getSnippetByAuthorAndName: (authorName: string, snippetName: string) => Snippet | undefined;
|
|
@@ -946,7 +946,7 @@ declare const createDatabase: ({ seed }?: {
|
|
|
946
946
|
step_function_name: string | null;
|
|
947
947
|
error_message: string | null;
|
|
948
948
|
}[];
|
|
949
|
-
}, "addOrder" | "getOrderById" | "getOrderFilesByOrderId" | "getJlcpcbOrderStatesByOrderId" | "getJlcpcbOrderStepRunsByJlcpcbOrderStateId" | "updateOrder" | "addJlcpcbOrderState" | "updateJlcpcbOrderState" | "addOrderFile" | "getOrderFileById" | "addAccount" | "addAccountPackage" | "getAccountPackageById" | "updateAccountPackage" | "deleteAccountPackage" | "addSnippet" | "
|
|
949
|
+
}, "addOrder" | "getOrderById" | "getOrderFilesByOrderId" | "getJlcpcbOrderStatesByOrderId" | "getJlcpcbOrderStepRunsByJlcpcbOrderStateId" | "updateOrder" | "addJlcpcbOrderState" | "updateJlcpcbOrderState" | "addOrderFile" | "getOrderFileById" | "addAccount" | "addAccountPackage" | "getAccountPackageById" | "updateAccountPackage" | "deleteAccountPackage" | "addSnippet" | "getLatestSnippets" | "getTrendingSnippets" | "getPackagesByAuthor" | "getSnippetByAuthorAndName" | "updateSnippet" | "getSnippetById" | "searchSnippets" | "deleteSnippet" | "addSession" | "getSessions" | "createLoginPage" | "getLoginPage" | "updateLoginPage" | "getAccount" | "updateAccount" | "createSession" | "addStar" | "removeStar" | "hasStarred" | "addPackage" | "updatePackage" | "getPackageById" | "getPackageReleaseById" | "addPackageRelease" | "updatePackageRelease" | "deletePackageFile" | "addPackageFile" | "updatePackageFile" | "getStarCount" | "getPackageFilesByReleaseId"> & {
|
|
950
950
|
addOrder: (order: Omit<Order, "order_id">) => Order;
|
|
951
951
|
getOrderById: (orderId: string) => Order | undefined;
|
|
952
952
|
getOrderFilesByOrderId: (orderId: string) => OrderFile[];
|
|
@@ -978,7 +978,7 @@ declare const createDatabase: ({ seed }?: {
|
|
|
978
978
|
updateAccountPackage: (accountPackageId: string, updates: Partial<AccountPackage>) => void;
|
|
979
979
|
deleteAccountPackage: (accountPackageId: string) => boolean;
|
|
980
980
|
addSnippet: (snippet: Omit<z.input<typeof snippetSchema>, "snippet_id" | "package_release_id">) => Snippet;
|
|
981
|
-
|
|
981
|
+
getLatestSnippets: (limit: number) => Snippet[];
|
|
982
982
|
getTrendingSnippets: (limit: number, since: string) => Snippet[];
|
|
983
983
|
getPackagesByAuthor: (authorName?: string) => Package[];
|
|
984
984
|
getSnippetByAuthorAndName: (authorName: string, snippetName: string) => Snippet | undefined;
|
package/dist/index.js
CHANGED
|
@@ -2209,7 +2209,7 @@ var initializer = combine(databaseSchema.parse({}), (set, get) => ({
|
|
|
2209
2209
|
is_unlisted: false
|
|
2210
2210
|
};
|
|
2211
2211
|
},
|
|
2212
|
-
|
|
2212
|
+
getLatestSnippets: (limit) => {
|
|
2213
2213
|
const state = get();
|
|
2214
2214
|
const snippetPackages = state.packages.filter((pkg) => pkg.is_snippet === true).map((pkg) => {
|
|
2215
2215
|
const packageRelease = state.packageReleases.find(
|
|
@@ -320,7 +320,7 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
|
|
|
320
320
|
is_unlisted: false,
|
|
321
321
|
}
|
|
322
322
|
},
|
|
323
|
-
|
|
323
|
+
getLatestSnippets: (limit: number): Snippet[] => {
|
|
324
324
|
const state = get()
|
|
325
325
|
|
|
326
326
|
// Get all packages that are snippets
|
|
@@ -26,9 +26,7 @@ export default withRouteSpec({
|
|
|
26
26
|
|
|
27
27
|
const pkg = ctx.db.packages[packageIndex]
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (pkg.owner_org_id !== ctx.auth.personal_org_id) {
|
|
29
|
+
if (pkg.owner_github_username !== ctx.auth.github_username) {
|
|
32
30
|
return ctx.error(403, {
|
|
33
31
|
error_code: "forbidden",
|
|
34
32
|
message: "You don't have permission to delete this package",
|
|
@@ -8,6 +8,6 @@ export default withRouteSpec({
|
|
|
8
8
|
snippets: z.array(snippetSchema),
|
|
9
9
|
}),
|
|
10
10
|
})(async (req, ctx) => {
|
|
11
|
-
const
|
|
12
|
-
return ctx.json({ snippets:
|
|
11
|
+
const latestSnippets = ctx.db.getLatestSnippets(20)
|
|
12
|
+
return ctx.json({ snippets: latestSnippets })
|
|
13
13
|
})
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ const staticRoutes = [
|
|
|
9
9
|
{ url: "/playground", changefreq: "weekly", priority: 0.9 },
|
|
10
10
|
{ url: "/quickstart", changefreq: "monthly", priority: 0.8 },
|
|
11
11
|
{ url: "/dashboard", changefreq: "weekly", priority: 0.7 },
|
|
12
|
-
{ url: "/
|
|
12
|
+
{ url: "/latest", changefreq: "daily", priority: 0.8 },
|
|
13
13
|
{ url: "/search", changefreq: "weekly", priority: 0.7 },
|
|
14
14
|
{ url: "/settings", changefreq: "monthly", priority: 0.6 },
|
|
15
15
|
{ url: "/community/join-redirect", changefreq: "monthly", priority: 0.6 },
|
package/src/App.tsx
CHANGED
|
@@ -61,7 +61,7 @@ const EditorPage = lazyImport(async () => {
|
|
|
61
61
|
})
|
|
62
62
|
const LandingPage = lazyImport(() => import("@/pages/landing"))
|
|
63
63
|
const MyOrdersPage = lazyImport(() => import("@/pages/my-orders"))
|
|
64
|
-
const
|
|
64
|
+
const LatestPage = lazyImport(() => import("@/pages/latest"))
|
|
65
65
|
const PreviewPage = lazyImport(() => import("@/pages/preview"))
|
|
66
66
|
const QuickstartPage = lazyImport(() => import("@/pages/quickstart"))
|
|
67
67
|
const SearchPage = lazyImport(() => import("@/pages/search"))
|
|
@@ -112,7 +112,7 @@ function App() {
|
|
|
112
112
|
<Route path="/quickstart" component={QuickstartPage} />
|
|
113
113
|
<Route path="/dashboard" component={DashboardPage} />
|
|
114
114
|
<Route path="/ai" component={AiPage} />
|
|
115
|
-
<Route path="/
|
|
115
|
+
<Route path="/latest" component={LatestPage} />
|
|
116
116
|
<Route path="/settings" component={SettingsPage} />
|
|
117
117
|
<Route path="/search" component={SearchPage} />
|
|
118
118
|
<Route path="/trending" component={TrendingPage} />
|
|
@@ -56,8 +56,8 @@ export default function Footer() {
|
|
|
56
56
|
<div className="space-y-4">
|
|
57
57
|
<h3 className="font-semibold uppercase">Explore</h3>
|
|
58
58
|
<footer className="flex flex-col space-y-2">
|
|
59
|
-
<PrefetchPageLink href="/
|
|
60
|
-
|
|
59
|
+
<PrefetchPageLink href="/latest" className="hover:underline">
|
|
60
|
+
Latest Snippets
|
|
61
61
|
</PrefetchPageLink>
|
|
62
62
|
<PrefetchPageLink href="/trending" className="hover:underline">
|
|
63
63
|
Trending Snippets
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from "@/components/ui/dropdown-menu"
|
|
9
|
+
import { Settings, Check } from "lucide-react"
|
|
10
|
+
|
|
11
|
+
interface HiddenFilesDropdownProps {
|
|
12
|
+
showHiddenFiles: boolean
|
|
13
|
+
onToggleHiddenFiles: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function HiddenFilesDropdown({
|
|
17
|
+
showHiddenFiles,
|
|
18
|
+
onToggleHiddenFiles,
|
|
19
|
+
}: HiddenFilesDropdownProps) {
|
|
20
|
+
return (
|
|
21
|
+
<DropdownMenu>
|
|
22
|
+
<DropdownMenuTrigger asChild>
|
|
23
|
+
<button className="ml-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 focus:outline-none">
|
|
24
|
+
<Settings className="h-4 w-4" />
|
|
25
|
+
</button>
|
|
26
|
+
</DropdownMenuTrigger>
|
|
27
|
+
<DropdownMenuContent
|
|
28
|
+
align="end"
|
|
29
|
+
className="bg-white dark:bg-[#161b22] border border-gray-200 dark:border-[#30363d] rounded-md shadow-lg"
|
|
30
|
+
>
|
|
31
|
+
<DropdownMenuItem
|
|
32
|
+
onSelect={(e) => e.preventDefault()}
|
|
33
|
+
onClick={onToggleHiddenFiles}
|
|
34
|
+
className="text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-[#21262d] flex items-center gap-2"
|
|
35
|
+
>
|
|
36
|
+
<Check
|
|
37
|
+
className={`h-4 w-4 transition-opacity ${showHiddenFiles ? "opacity-100" : "opacity-0"}`}
|
|
38
|
+
/>
|
|
39
|
+
Show Hidden Files
|
|
40
|
+
</DropdownMenuItem>
|
|
41
|
+
</DropdownMenuContent>
|
|
42
|
+
</DropdownMenu>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -11,7 +11,7 @@ export const LatestSnippets: React.FC = () => {
|
|
|
11
11
|
isLoading,
|
|
12
12
|
error,
|
|
13
13
|
} = useQuery<Snippet[]>("latestSnippets", async () => {
|
|
14
|
-
const response = await axios.get("/snippets/
|
|
14
|
+
const response = await axios.get("/snippets/list_latest")
|
|
15
15
|
return response.data.snippets
|
|
16
16
|
})
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ export default function BOMView() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<div className="border border-gray-200 dark:border-[#30363d] rounded-md p-4 mb-4 bg-white dark:bg-[#0d1117] w-full h-[620px]">
|
|
26
|
+
<div className="border border-gray-200 dark:border-[#30363d] rounded-md p-4 mb-4 bg-white dark:bg-[#0d1117] w-full h-[620px] overflow-y-scroll">
|
|
27
27
|
<BomTable circuitJson={circuitJson} />
|
|
28
28
|
</div>
|
|
29
29
|
)
|
|
@@ -5,6 +5,14 @@ import { FileText, Folder } from "lucide-react"
|
|
|
5
5
|
import { useMemo, useState } from "react"
|
|
6
6
|
import { isHiddenFile } from "../../utils/is-hidden-file"
|
|
7
7
|
import { isWithinDirectory } from "../../utils/is-within-directory"
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@/components/ui/dropdown-menu"
|
|
14
|
+
import { Settings } from "lucide-react"
|
|
15
|
+
import HiddenFilesDropdown from "@/components/HiddenFilesDropdown"
|
|
8
16
|
|
|
9
17
|
interface Directory {
|
|
10
18
|
type: "directory"
|
|
@@ -41,6 +49,7 @@ export default function FilesView({
|
|
|
41
49
|
onFileClicked,
|
|
42
50
|
}: FilesViewProps) {
|
|
43
51
|
const [activeDir, setActiveDir] = useState("")
|
|
52
|
+
const [showHiddenFiles, setShowHiddenFiles] = useState(false)
|
|
44
53
|
|
|
45
54
|
// Parse package files to determine directories and files structure
|
|
46
55
|
const { directories, files } = useMemo(() => {
|
|
@@ -52,7 +61,7 @@ export default function FilesView({
|
|
|
52
61
|
const filesList: File[] = []
|
|
53
62
|
|
|
54
63
|
packageFiles
|
|
55
|
-
.filter((file) => !isHiddenFile(file.file_path))
|
|
64
|
+
.filter((file) => showHiddenFiles || !isHiddenFile(file.file_path))
|
|
56
65
|
.forEach((file) => {
|
|
57
66
|
// Extract directory path
|
|
58
67
|
const pathParts = file.file_path.split("/")
|
|
@@ -64,6 +73,7 @@ export default function FilesView({
|
|
|
64
73
|
currentPath += (currentPath ? "/" : "") + part
|
|
65
74
|
// Only add directory if it contains visible files
|
|
66
75
|
if (
|
|
76
|
+
showHiddenFiles ||
|
|
67
77
|
packageFiles.some(
|
|
68
78
|
(f) =>
|
|
69
79
|
f.file_path.startsWith(currentPath + "/") &&
|
|
@@ -100,7 +110,7 @@ export default function FilesView({
|
|
|
100
110
|
directories: dirsList,
|
|
101
111
|
files: filesList,
|
|
102
112
|
}
|
|
103
|
-
}, [packageFiles])
|
|
113
|
+
}, [packageFiles, showHiddenFiles])
|
|
104
114
|
// Format date for display
|
|
105
115
|
const formatDate = (dateString: string) => {
|
|
106
116
|
const date = new Date(dateString)
|
|
@@ -155,6 +165,8 @@ export default function FilesView({
|
|
|
155
165
|
setActiveDir(parentDir)
|
|
156
166
|
}
|
|
157
167
|
|
|
168
|
+
const toggleHiddenFiles = () => setShowHiddenFiles((prev) => !prev)
|
|
169
|
+
|
|
158
170
|
if (isLoading) {
|
|
159
171
|
return (
|
|
160
172
|
<div className="mb-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
|
|
@@ -197,6 +209,11 @@ export default function FilesView({
|
|
|
197
209
|
<span>
|
|
198
210
|
{files.length} files, {directories.length} directories
|
|
199
211
|
</span>
|
|
212
|
+
|
|
213
|
+
<HiddenFilesDropdown
|
|
214
|
+
showHiddenFiles={showHiddenFiles}
|
|
215
|
+
onToggleHiddenFiles={toggleHiddenFiles}
|
|
216
|
+
/>
|
|
200
217
|
</div>
|
|
201
218
|
|
|
202
219
|
{/* Mobile view */}
|
|
@@ -208,6 +225,10 @@ export default function FilesView({
|
|
|
208
225
|
</div>
|
|
209
226
|
<div className="flex items-center text-xs text-gray-500 dark:text-[#8b949e]">
|
|
210
227
|
<span>{files.length + directories.length} items</span>
|
|
228
|
+
<HiddenFilesDropdown
|
|
229
|
+
showHiddenFiles={showHiddenFiles}
|
|
230
|
+
onToggleHiddenFiles={toggleHiddenFiles}
|
|
231
|
+
/>
|
|
211
232
|
</div>
|
|
212
233
|
</div>
|
|
213
234
|
</div>
|
package/src/pages/dashboard.tsx
CHANGED
|
@@ -18,7 +18,7 @@ export const DashboardPage = () => {
|
|
|
18
18
|
|
|
19
19
|
const currentUser = useGlobalStore((s) => s.session?.github_username)
|
|
20
20
|
const [showAllTrending, setShowAllTrending] = useState(false)
|
|
21
|
-
const [
|
|
21
|
+
const [showAllLatest, setShowAllLatest] = useState(false)
|
|
22
22
|
|
|
23
23
|
const {
|
|
24
24
|
data: mySnippets,
|
|
@@ -41,10 +41,10 @@ export const DashboardPage = () => {
|
|
|
41
41
|
},
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
const { data:
|
|
45
|
-
"
|
|
44
|
+
const { data: latestSnippets } = useQuery<Snippet[]>(
|
|
45
|
+
"latestSnippets",
|
|
46
46
|
async () => {
|
|
47
|
-
const response = await axios.get("/snippets/
|
|
47
|
+
const response = await axios.get("/snippets/list_latest")
|
|
48
48
|
return response.data.snippets
|
|
49
49
|
},
|
|
50
50
|
)
|
|
@@ -129,10 +129,10 @@ export const DashboardPage = () => {
|
|
|
129
129
|
/>
|
|
130
130
|
<div className="mt-8">
|
|
131
131
|
<SnippetList
|
|
132
|
-
title="
|
|
133
|
-
snippets={
|
|
134
|
-
showAll={
|
|
135
|
-
onToggleShowAll={() =>
|
|
132
|
+
title="Latest Snippets"
|
|
133
|
+
snippets={latestSnippets}
|
|
134
|
+
showAll={showAllLatest}
|
|
135
|
+
onToggleShowAll={() => setShowAllLatest(!showAllLatest)}
|
|
136
136
|
/>
|
|
137
137
|
</div>
|
|
138
138
|
</div>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import React, { 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 {
|
|
8
|
+
Search,
|
|
9
|
+
Tag,
|
|
10
|
+
Calendar,
|
|
11
|
+
Keyboard,
|
|
12
|
+
Cpu,
|
|
13
|
+
Layers,
|
|
14
|
+
LucideBellElectric,
|
|
15
|
+
} from "lucide-react"
|
|
16
|
+
import { Input } from "@/components/ui/input"
|
|
17
|
+
import { Badge } from "@/components/ui/badge"
|
|
18
|
+
import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select"
|
|
26
|
+
import { SnippetCard } from "@/components/SnippetCard"
|
|
27
|
+
|
|
28
|
+
const LatestPage: React.FC = () => {
|
|
29
|
+
const axios = useAxios()
|
|
30
|
+
const apiBaseUrl = useSnippetsBaseApiUrl()
|
|
31
|
+
const [searchQuery, setSearchQuery] = useState("")
|
|
32
|
+
const [category, setCategory] = useState("all")
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
data: snippets,
|
|
36
|
+
isLoading,
|
|
37
|
+
error,
|
|
38
|
+
} = useQuery<Snippet[]>(
|
|
39
|
+
["latestSnippets", category],
|
|
40
|
+
async () => {
|
|
41
|
+
const params = category !== "all" ? { tag: category } : {}
|
|
42
|
+
const response = await axios.get("/snippets/list_latest", { params })
|
|
43
|
+
return response.data.snippets
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
keepPreviousData: true,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const filteredSnippets = snippets?.filter((snippet) => {
|
|
51
|
+
if (!searchQuery) return true
|
|
52
|
+
|
|
53
|
+
const query = searchQuery.toLowerCase().trim()
|
|
54
|
+
|
|
55
|
+
const searchableFields = [
|
|
56
|
+
snippet.unscoped_name.toLowerCase(),
|
|
57
|
+
snippet.owner_name.toLowerCase(),
|
|
58
|
+
(snippet.description || "").toLowerCase(),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
return searchableFields.some((field) => {
|
|
62
|
+
const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
|
|
63
|
+
return queryWords.every((word) => field.includes(word))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="min-h-screen flex flex-col bg-gray-50">
|
|
69
|
+
<Header />
|
|
70
|
+
<main className="flex-grow container mx-auto px-4 py-8">
|
|
71
|
+
<div className="mb-8 max-w-3xl">
|
|
72
|
+
<div className="flex items-center gap-2 mb-3">
|
|
73
|
+
<Calendar className="w-6 h-6 text-blue-500" />
|
|
74
|
+
<h1 className="text-4xl font-bold text-gray-900">
|
|
75
|
+
Latest Snippets
|
|
76
|
+
</h1>
|
|
77
|
+
</div>
|
|
78
|
+
<p className="text-lg text-gray-600 mb-4">
|
|
79
|
+
Explore the latest circuit designs from our community. These fresh
|
|
80
|
+
additions showcase new ideas and innovative approaches to circuit
|
|
81
|
+
design.
|
|
82
|
+
</p>
|
|
83
|
+
<div className="flex flex-wrap gap-3">
|
|
84
|
+
<Badge variant="secondary" className="px-3 py-1">
|
|
85
|
+
<Tag className="w-3.5 h-3.5 mr-1" />
|
|
86
|
+
<span>Latest Uploads</span>
|
|
87
|
+
</Badge>
|
|
88
|
+
<Badge variant="secondary" className="px-3 py-1">
|
|
89
|
+
<Calendar className="w-3.5 h-3.5 mr-1" />
|
|
90
|
+
<span>Most Recent First</span>
|
|
91
|
+
</Badge>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="mb-6">
|
|
96
|
+
<div className="flex flex-col sm:flex-row gap-4 mb-4">
|
|
97
|
+
<div className="relative flex-grow">
|
|
98
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
|
99
|
+
<Input
|
|
100
|
+
type="text"
|
|
101
|
+
placeholder="Search latest snippets..."
|
|
102
|
+
value={searchQuery}
|
|
103
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
104
|
+
className="pl-10"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
<Select value={category} onValueChange={setCategory}>
|
|
108
|
+
<SelectTrigger className="w-full sm:w-[180px]">
|
|
109
|
+
<SelectValue placeholder="Category" />
|
|
110
|
+
</SelectTrigger>
|
|
111
|
+
<SelectContent>
|
|
112
|
+
<SelectItem value="all">All Categories</SelectItem>
|
|
113
|
+
<SelectItem value="keyboard">
|
|
114
|
+
<div className="flex items-center">
|
|
115
|
+
<Keyboard className="mr-2 h-4 w-4" />
|
|
116
|
+
<span>Keyboards</span>
|
|
117
|
+
</div>
|
|
118
|
+
</SelectItem>
|
|
119
|
+
<SelectItem value="microcontroller">
|
|
120
|
+
<div className="flex items-center">
|
|
121
|
+
<Cpu className="mr-2 h-4 w-4" />
|
|
122
|
+
<span>Microcontrollers</span>
|
|
123
|
+
</div>
|
|
124
|
+
</SelectItem>
|
|
125
|
+
<SelectItem value="connector">
|
|
126
|
+
<div className="flex items-center">
|
|
127
|
+
<Layers className="mr-2 h-4 w-4" />
|
|
128
|
+
<span>Connectors</span>
|
|
129
|
+
</div>
|
|
130
|
+
</SelectItem>
|
|
131
|
+
<SelectItem value="sensor">
|
|
132
|
+
<div className="flex items-center">
|
|
133
|
+
<LucideBellElectric className="mr-2 h-4 w-4" />
|
|
134
|
+
<span>Sensors</span>
|
|
135
|
+
</div>
|
|
136
|
+
</SelectItem>
|
|
137
|
+
</SelectContent>
|
|
138
|
+
</Select>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{isLoading ? (
|
|
143
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
144
|
+
{[...Array(6)].map((_, i) => (
|
|
145
|
+
<div key={i} className="border p-4 rounded-md animate-pulse">
|
|
146
|
+
<div className="flex items-start gap-4">
|
|
147
|
+
<div className="h-16 w-16 flex-shrink-0 rounded-md bg-slate-200"></div>
|
|
148
|
+
<div className="flex-1">
|
|
149
|
+
<div className="h-5 bg-slate-200 rounded w-3/4 mb-2"></div>
|
|
150
|
+
<div className="h-4 bg-slate-200 rounded w-1/2 mb-2"></div>
|
|
151
|
+
<div className="flex gap-2">
|
|
152
|
+
<div className="h-3 bg-slate-200 rounded w-16"></div>
|
|
153
|
+
<div className="h-3 bg-slate-200 rounded w-16"></div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
) : error ? (
|
|
161
|
+
<div className="bg-red-50 border border-red-200 text-red-700 p-6 rounded-xl shadow-sm max-w-2xl mx-auto">
|
|
162
|
+
<div className="flex items-start">
|
|
163
|
+
<div className="mr-4 bg-red-100 p-2 rounded-full">
|
|
164
|
+
<Search className="w-6 h-6 text-red-600" />
|
|
165
|
+
</div>
|
|
166
|
+
<div>
|
|
167
|
+
<h3 className="text-lg font-semibold mb-2">
|
|
168
|
+
Error Loading Snippets
|
|
169
|
+
</h3>
|
|
170
|
+
<p className="text-red-600">
|
|
171
|
+
We couldn't load the latest snippets. Please try again later.
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
) : filteredSnippets?.length === 0 ? (
|
|
177
|
+
<div className="text-center py-12 px-4">
|
|
178
|
+
<div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
|
|
179
|
+
<Search className="w-8 h-8 text-slate-400" />
|
|
180
|
+
</div>
|
|
181
|
+
<h3 className="text-xl font-medium text-slate-900 mb-2">
|
|
182
|
+
No Matching Snippets
|
|
183
|
+
</h3>
|
|
184
|
+
<p className="text-slate-500 max-w-md mx-auto mb-6">
|
|
185
|
+
{searchQuery
|
|
186
|
+
? `No snippets match your search for "${searchQuery}".`
|
|
187
|
+
: category !== "all"
|
|
188
|
+
? `No ${category} snippets found in the latest list.`
|
|
189
|
+
: "There are no new snippets at the moment."}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
) : (
|
|
193
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
194
|
+
{filteredSnippets
|
|
195
|
+
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
196
|
+
?.map((snippet) => (
|
|
197
|
+
<SnippetCard
|
|
198
|
+
key={snippet.snippet_id}
|
|
199
|
+
snippet={snippet}
|
|
200
|
+
baseUrl={apiBaseUrl}
|
|
201
|
+
showOwner={true}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</main>
|
|
207
|
+
<Footer />
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default LatestPage
|
package/src/pages/newest.tsx
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import Header from "@/components/Header"
|
|
2
|
-
import Footer from "@/components/Footer"
|
|
3
|
-
import { LatestSnippets } from "@/components/LatestSnippets"
|
|
4
|
-
|
|
5
|
-
export const NewestPage = () => {
|
|
6
|
-
return (
|
|
7
|
-
<div>
|
|
8
|
-
<Header />
|
|
9
|
-
<div className="container mx-auto px-4 py-8">
|
|
10
|
-
<h1 className="text-3xl font-bold mb-6">Newest Snippets</h1>
|
|
11
|
-
<LatestSnippets />
|
|
12
|
-
</div>
|
|
13
|
-
<Footer />
|
|
14
|
-
</div>
|
|
15
|
-
)
|
|
16
|
-
}
|