@tscircuit/fake-snippets 0.0.36 → 0.0.38
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/package_files/create_or_update.test.ts +575 -0
- package/bun-tests/fake-snippets-api/routes/package_files/delete.test.ts +233 -0
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +2 -2
- package/dist/bundle.js +499 -249
- package/dist/index.d.ts +23 -4
- package/dist/index.js +30 -2
- package/fake-snippets-api/lib/db/db-client.ts +29 -1
- package/fake-snippets-api/lib/db/schema.ts +3 -0
- package/fake-snippets-api/routes/api/package_files/create_or_update.ts +179 -0
- package/fake-snippets-api/routes/api/package_files/delete.ts +106 -0
- 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/EditorNav.tsx +4 -4
- package/src/components/Footer.tsx +9 -3
- package/src/components/HiddenFilesDropdown.tsx +44 -0
- package/src/components/LatestSnippets.tsx +1 -1
- package/src/components/ViewPackagePage/components/package-header.tsx +0 -4
- 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/components/dialogs/confirm-delete-package-dialog.tsx +48 -0
- package/src/hooks/use-delete-package.ts +40 -0
- package/src/hooks/use-fork-package-mutation.ts +14 -61
- package/src/pages/dashboard.tsx +8 -8
- package/src/pages/latest.tsx +212 -0
- package/src/pages/user-profile.tsx +15 -14
- package/src/components/dialogs/confirm-delete-snippet-dialog.tsx +0 -80
- package/src/pages/newest.tsx +0 -16
|
@@ -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
|
|
|
@@ -61,10 +61,6 @@ export default function PackageHeader({
|
|
|
61
61
|
const handleForkClick = async () => {
|
|
62
62
|
if (!packageInfo?.package_id) return
|
|
63
63
|
await forkPackage(packageInfo.package_id)
|
|
64
|
-
toast({
|
|
65
|
-
title: "Forked package",
|
|
66
|
-
description: "Package forked successfully",
|
|
67
|
-
})
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
const isStarLoading =
|
|
@@ -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>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
|
|
2
|
+
import { Button } from "../ui/button"
|
|
3
|
+
import { createUseDialog } from "./create-use-dialog"
|
|
4
|
+
import { useDeletePackage } from "@/hooks/use-delete-package"
|
|
5
|
+
|
|
6
|
+
export const ConfirmDeletePackageDialog = ({
|
|
7
|
+
open,
|
|
8
|
+
onOpenChange,
|
|
9
|
+
packageId,
|
|
10
|
+
packageName,
|
|
11
|
+
}: {
|
|
12
|
+
open: boolean
|
|
13
|
+
onOpenChange: (open: boolean) => void
|
|
14
|
+
packageId: string
|
|
15
|
+
packageName: string
|
|
16
|
+
}) => {
|
|
17
|
+
const { mutate: deletePackage, isLoading } = useDeletePackage({
|
|
18
|
+
onSuccess: () => onOpenChange(false),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
23
|
+
<DialogContent>
|
|
24
|
+
<DialogHeader>
|
|
25
|
+
<DialogTitle>Confirm Delete Package</DialogTitle>
|
|
26
|
+
</DialogHeader>
|
|
27
|
+
<p>Are you sure you want to delete the package "{packageName}"?</p>
|
|
28
|
+
<p>This action cannot be undone.</p>
|
|
29
|
+
<div className="flex justify-end space-x-2 mt-4">
|
|
30
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
31
|
+
Cancel
|
|
32
|
+
</Button>
|
|
33
|
+
<Button
|
|
34
|
+
variant="destructive"
|
|
35
|
+
onClick={() => deletePackage({ package_id: packageId })}
|
|
36
|
+
disabled={isLoading}
|
|
37
|
+
>
|
|
38
|
+
{isLoading ? "Deleting..." : "Delete"}
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
</DialogContent>
|
|
42
|
+
</Dialog>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const useConfirmDeletePackageDialog = createUseDialog(
|
|
47
|
+
ConfirmDeletePackageDialog,
|
|
48
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMutation } from "react-query"
|
|
2
|
+
import { useAxios } from "./use-axios"
|
|
3
|
+
import { useToast } from "./use-toast"
|
|
4
|
+
|
|
5
|
+
export const useDeletePackage = ({
|
|
6
|
+
onSuccess,
|
|
7
|
+
}: { onSuccess?: () => void } = {}) => {
|
|
8
|
+
const axios = useAxios()
|
|
9
|
+
const { toast } = useToast()
|
|
10
|
+
|
|
11
|
+
return useMutation(
|
|
12
|
+
["deletePackage"],
|
|
13
|
+
async ({ package_id }: { package_id: string }) => {
|
|
14
|
+
const response = await axios.post("/packages/delete", {
|
|
15
|
+
package_id,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (!response.data.ok) {
|
|
19
|
+
throw new Error("Failed to delete package")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return response.data
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
onSuccess: () => {
|
|
26
|
+
toast({
|
|
27
|
+
title: "Package deleted",
|
|
28
|
+
description: "Package deleted successfully",
|
|
29
|
+
})
|
|
30
|
+
onSuccess?.()
|
|
31
|
+
},
|
|
32
|
+
onError: (error: any) => {
|
|
33
|
+
toast({
|
|
34
|
+
title: "Error deleting package",
|
|
35
|
+
description: "Failed to delete package",
|
|
36
|
+
})
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Package,
|
|
3
|
-
PackageFile,
|
|
4
|
-
PackageRelease,
|
|
5
|
-
} from "fake-snippets-api/lib/db/schema"
|
|
1
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
6
2
|
import { useMutation } from "react-query"
|
|
7
3
|
import { useAxios } from "./use-axios"
|
|
8
4
|
import { useGlobalStore } from "./use-global-store"
|
|
9
5
|
import { useToast } from "./use-toast"
|
|
10
|
-
import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
11
|
-
import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
|
|
12
|
-
import { useCreatePackageFilesMutation } from "./use-create-package-files-mutation"
|
|
13
6
|
|
|
14
7
|
export const useForkPackageMutation = ({
|
|
15
8
|
onSuccess,
|
|
@@ -20,73 +13,33 @@ export const useForkPackageMutation = ({
|
|
|
20
13
|
const session = useGlobalStore((s) => s.session)
|
|
21
14
|
const { toast } = useToast()
|
|
22
15
|
|
|
23
|
-
const { mutateAsync: createPackage } = useCreatePackageMutation()
|
|
24
|
-
const { mutateAsync: createRelease } = useCreatePackageReleaseMutation()
|
|
25
|
-
const { mutateAsync: createFile } = useCreatePackageFilesMutation()
|
|
26
|
-
|
|
27
16
|
return useMutation(
|
|
28
17
|
["forkPackage"],
|
|
29
18
|
async (packageId: string) => {
|
|
30
19
|
if (!session) throw new Error("No session")
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
params: { package_id: packageId },
|
|
35
|
-
})
|
|
36
|
-
const sourcePackage: Package = packageData.package
|
|
37
|
-
if (!sourcePackage) throw new Error("Source package not found")
|
|
38
|
-
|
|
39
|
-
// Step 2: Fetch latest release
|
|
40
|
-
const { data: releaseData } = await axios.post("/package_releases/get", {
|
|
41
|
-
package_release_id: sourcePackage.latest_package_release_id,
|
|
21
|
+
const { data } = await axios.post("/packages/fork", {
|
|
22
|
+
package_id: packageId,
|
|
42
23
|
})
|
|
43
|
-
const sourceRelease: PackageRelease = releaseData.package_release
|
|
44
|
-
if (!sourceRelease) throw new Error("Source release not found")
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
package_release_id: sourceRelease.package_release_id,
|
|
49
|
-
})
|
|
50
|
-
const sourceFiles: PackageFile[] = filesData.package_files
|
|
51
|
-
if (!sourceFiles?.length) throw new Error("No source files found")
|
|
52
|
-
|
|
53
|
-
// Step 4: Create new package
|
|
54
|
-
const newPackage = await createPackage({
|
|
55
|
-
name: `${session.github_username}/${sourcePackage.unscoped_name}`,
|
|
56
|
-
description: `Fork of ${sourcePackage.name}`,
|
|
57
|
-
})
|
|
25
|
+
const forkedPackage: Package = data.package
|
|
26
|
+
if (!forkedPackage) throw new Error("Failed to fork package")
|
|
58
27
|
|
|
59
|
-
|
|
60
|
-
const newRelease = await createRelease({
|
|
61
|
-
package_id: newPackage.package_id,
|
|
62
|
-
version: sourceRelease.version ?? undefined,
|
|
63
|
-
is_latest: true,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
// Step 6: Create all files
|
|
67
|
-
const newFiles = await Promise.all(
|
|
68
|
-
sourceFiles.map((file: PackageFile) =>
|
|
69
|
-
createFile({
|
|
70
|
-
package_release_id: newRelease.package_release_id,
|
|
71
|
-
file_path: file.file_path,
|
|
72
|
-
content_text: file.content_text ?? undefined,
|
|
73
|
-
}),
|
|
74
|
-
),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
package: newPackage,
|
|
79
|
-
release: newRelease,
|
|
80
|
-
files: newFiles,
|
|
81
|
-
}
|
|
28
|
+
return forkedPackage
|
|
82
29
|
},
|
|
83
30
|
{
|
|
84
31
|
onSuccess: (result) => {
|
|
85
32
|
toast({
|
|
86
33
|
title: "Package Forked",
|
|
87
|
-
description: `Successfully forked package to @${session?.github_username}/${result.
|
|
34
|
+
description: `Successfully forked package to @${session?.github_username}/${result.unscoped_name}`,
|
|
88
35
|
})
|
|
89
|
-
|
|
36
|
+
|
|
37
|
+
const url = new URL(window.location.href)
|
|
38
|
+
url.pathname = `/${session?.github_username}/${result.unscoped_name}`
|
|
39
|
+
url.search = ""
|
|
40
|
+
window.location.href = url.toString()
|
|
41
|
+
|
|
42
|
+
onSuccess?.(result)
|
|
90
43
|
},
|
|
91
44
|
onError: (error: any) => {
|
|
92
45
|
toast({
|
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
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import React, { useState } from "react"
|
|
2
|
-
import { useParams } from "wouter"
|
|
3
|
-
import { useQuery } from "react-query"
|
|
4
|
-
import { useAxios } from "@/hooks/use-axios"
|
|
5
|
-
import Header from "@/components/Header"
|
|
6
1
|
import Footer from "@/components/Footer"
|
|
7
|
-
import
|
|
2
|
+
import Header from "@/components/Header"
|
|
3
|
+
import { SnippetCard } from "@/components/SnippetCard"
|
|
4
|
+
import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
8
6
|
import { Button } from "@/components/ui/button"
|
|
9
|
-
import { GitHubLogoIcon } from "@radix-ui/react-icons"
|
|
10
7
|
import { Input } from "@/components/ui/input"
|
|
11
|
-
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
12
|
-
import { useConfirmDeleteSnippetDialog } from "@/components/dialogs/confirm-delete-snippet-dialog"
|
|
13
|
-
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
|
|
14
8
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
9
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
10
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
15
11
|
import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
|
|
16
|
-
import {
|
|
12
|
+
import { GitHubLogoIcon } from "@radix-ui/react-icons"
|
|
13
|
+
import type { Snippet } from "fake-snippets-api/lib/db/schema"
|
|
14
|
+
import type React from "react"
|
|
15
|
+
import { useState } from "react"
|
|
16
|
+
import { useQuery } from "react-query"
|
|
17
|
+
import { useParams } from "wouter"
|
|
17
18
|
|
|
18
19
|
export const UserProfilePage = () => {
|
|
19
20
|
const { username } = useParams()
|
|
@@ -23,7 +24,7 @@ export const UserProfilePage = () => {
|
|
|
23
24
|
const session = useGlobalStore((s) => s.session)
|
|
24
25
|
const isCurrentUserProfile = username === session?.github_username
|
|
25
26
|
const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
|
|
26
|
-
|
|
27
|
+
useConfirmDeletePackageDialog()
|
|
27
28
|
const [snippetToDelete, setSnippetToDelete] = useState<Snippet | null>(null)
|
|
28
29
|
|
|
29
30
|
const { data: userSnippets, isLoading: isLoadingUserSnippets } = useQuery<
|
|
@@ -138,8 +139,8 @@ export const UserProfilePage = () => {
|
|
|
138
139
|
</div>
|
|
139
140
|
{snippetToDelete && (
|
|
140
141
|
<DeleteDialog
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
packageId={snippetToDelete.snippet_id}
|
|
143
|
+
packageName={snippetToDelete.unscoped_name}
|
|
143
144
|
/>
|
|
144
145
|
)}
|
|
145
146
|
<Footer />
|