@tscircuit/fake-snippets 0.0.110 → 0.0.111
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/fixtures/get-test-server.ts +1 -0
- package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +2 -2
- package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +52 -0
- package/bun.lock +14 -2
- package/dist/bundle.js +81 -36
- package/dist/index.d.ts +33 -13
- package/dist/index.js +10 -6
- package/dist/schema.d.ts +24 -8
- package/dist/schema.js +5 -3
- package/fake-snippets-api/lib/db/db-client.ts +7 -3
- package/fake-snippets-api/lib/db/schema.ts +3 -2
- package/fake-snippets-api/lib/middleware/with-session-auth.ts +59 -7
- package/fake-snippets-api/lib/public-mapping/public-map-org.ts +3 -2
- package/fake-snippets-api/routes/api/orgs/create.ts +4 -2
- package/fake-snippets-api/routes/api/orgs/get.ts +1 -1
- package/fake-snippets-api/routes/api/orgs/list_members.ts +1 -8
- package/fake-snippets-api/routes/api/orgs/update.ts +31 -6
- package/fake-snippets-api/routes/api/packages/create.ts +5 -2
- package/package.json +6 -4
- package/src/App.tsx +3 -5
- package/src/components/DownloadButtonAndMenu.tsx +14 -1
- package/src/components/SentryNotFoundReporter.tsx +44 -0
- package/src/components/organization/OrganizationCard.tsx +5 -3
- package/src/hooks/use-hydration.ts +30 -0
- package/src/hooks/use-org-by-github-handle.ts +1 -1
- package/src/lib/download-fns/download-kicad-files.ts +10 -0
- package/src/lib/download-fns/download-step.ts +12 -0
- package/src/pages/create-organization.tsx +1 -0
- package/src/pages/organization-settings.tsx +4 -1
- package/src/pages/search.tsx +7 -2
- package/src/pages/user-settings.tsx +161 -0
- package/src/pages/view-package.tsx +23 -3
|
@@ -13,6 +13,7 @@ export default withRouteSpec({
|
|
|
13
13
|
z.object({
|
|
14
14
|
name: z.string().optional(),
|
|
15
15
|
display_name: z.string().optional(),
|
|
16
|
+
github_handle: z.string().trim().min(1).nullable().optional(),
|
|
16
17
|
}),
|
|
17
18
|
),
|
|
18
19
|
auth: "session",
|
|
@@ -20,10 +21,11 @@ export default withRouteSpec({
|
|
|
20
21
|
org: publicOrgSchema,
|
|
21
22
|
}),
|
|
22
23
|
})(async (req, ctx) => {
|
|
23
|
-
const { org_id, name, display_name } = req.commonParams as {
|
|
24
|
+
const { org_id, name, display_name, github_handle } = req.commonParams as {
|
|
24
25
|
org_id: string
|
|
25
26
|
name?: string
|
|
26
27
|
display_name?: string
|
|
28
|
+
github_handle?: string
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
const org = ctx.db.getOrg({ org_id }, ctx.auth)
|
|
@@ -43,13 +45,13 @@ export default withRouteSpec({
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// No changes provided
|
|
46
|
-
if (!name && display_name === undefined) {
|
|
48
|
+
if (!name && display_name === undefined && github_handle === null) {
|
|
47
49
|
return ctx.json({ org: publicMapOrg(org) })
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
if (name && name !== org.
|
|
52
|
+
if (name && name !== org.org_name) {
|
|
51
53
|
// Validate duplicate name
|
|
52
|
-
const duplicate = ctx.db.getOrg({
|
|
54
|
+
const duplicate = ctx.db.getOrg({ org_name: name })
|
|
53
55
|
|
|
54
56
|
if (duplicate && duplicate.org_id !== org_id) {
|
|
55
57
|
return ctx.error(400, {
|
|
@@ -58,20 +60,43 @@ export default withRouteSpec({
|
|
|
58
60
|
})
|
|
59
61
|
}
|
|
60
62
|
}
|
|
63
|
+
if (
|
|
64
|
+
github_handle !== undefined &&
|
|
65
|
+
github_handle !== org.github_handle &&
|
|
66
|
+
github_handle !== null
|
|
67
|
+
) {
|
|
68
|
+
const duplicateHandle = ctx.db.getOrg({ github_handle })
|
|
69
|
+
? ctx.db.getOrg({ github_handle })?.org_id != org_id
|
|
70
|
+
: false
|
|
61
71
|
|
|
72
|
+
if (duplicateHandle) {
|
|
73
|
+
return ctx.error(400, {
|
|
74
|
+
error_code: "org_github_handle_already_exists",
|
|
75
|
+
message: "An organization with this GitHub handle already exists",
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
62
79
|
const updates: {
|
|
63
|
-
|
|
80
|
+
org_name?: string
|
|
64
81
|
org_display_name?: string
|
|
82
|
+
github_handle?: string
|
|
65
83
|
} = {}
|
|
66
84
|
|
|
67
85
|
if (name) {
|
|
68
86
|
updates.github_handle = name
|
|
87
|
+
updates.org_name = name
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (github_handle !== undefined) {
|
|
91
|
+
updates.github_handle = github_handle
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
if (display_name !== undefined) {
|
|
72
95
|
const trimmedDisplayName = display_name.trim()
|
|
96
|
+
const handleForFallback =
|
|
97
|
+
github_handle !== undefined ? github_handle : org.github_handle
|
|
73
98
|
const fallbackDisplayName =
|
|
74
|
-
name ?? org.org_display_name ?? org.
|
|
99
|
+
name ?? org.org_display_name ?? org.org_name ?? handleForFallback ?? ""
|
|
75
100
|
updates.org_display_name =
|
|
76
101
|
trimmedDisplayName.length > 0 ? trimmedDisplayName : fallbackDisplayName
|
|
77
102
|
}
|
|
@@ -66,6 +66,7 @@ export default withRouteSpec({
|
|
|
66
66
|
.find(
|
|
67
67
|
(o) =>
|
|
68
68
|
o.org_display_name?.toLowerCase() === requested_owner_lower ||
|
|
69
|
+
o.org_name?.toLowerCase() === requested_owner_lower ||
|
|
69
70
|
o.github_handle?.toLowerCase() === requested_owner_lower,
|
|
70
71
|
)
|
|
71
72
|
|
|
@@ -78,10 +79,12 @@ export default withRouteSpec({
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
owner_org_id = memberOrg.org_id
|
|
81
|
-
owner_github_username = memberOrg.github_handle
|
|
82
|
+
owner_github_username = memberOrg.github_handle || memberOrg.org_name
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
const existingPackage = ctx.db
|
|
85
|
+
const existingPackage = ctx.db
|
|
86
|
+
.getState()
|
|
87
|
+
.packages.find((pkg) => pkg.name === final_name)
|
|
85
88
|
|
|
86
89
|
if (existingPackage) {
|
|
87
90
|
throw ctx.error(400, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fake-snippets",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.111",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -110,10 +110,12 @@
|
|
|
110
110
|
"circuit-json-to-bom-csv": "^0.0.7",
|
|
111
111
|
"circuit-json-to-gerber": "^0.0.29",
|
|
112
112
|
"circuit-json-to-gltf": "^0.0.14",
|
|
113
|
+
"circuit-json-to-kicad": "^0.0.9",
|
|
113
114
|
"circuit-json-to-pnp-csv": "^0.0.7",
|
|
114
115
|
"circuit-json-to-readable-netlist": "^0.0.13",
|
|
115
116
|
"circuit-json-to-simple-3d": "^0.0.7",
|
|
116
117
|
"circuit-json-to-spice": "^0.0.6",
|
|
118
|
+
"circuit-json-to-step": "^0.0.2",
|
|
117
119
|
"circuit-json-to-tscircuit": "^0.0.9",
|
|
118
120
|
"circuit-to-svg": "^0.0.202",
|
|
119
121
|
"class-variance-authority": "^0.7.1",
|
|
@@ -139,6 +141,7 @@
|
|
|
139
141
|
"jscad-electronics": "^0.0.25",
|
|
140
142
|
"jszip": "^3.10.1",
|
|
141
143
|
"kicad-converter": "^0.0.17",
|
|
144
|
+
"kicadts": "^0.0.9",
|
|
142
145
|
"ky": "^1.7.5",
|
|
143
146
|
"lucide-react": "^0.488.0",
|
|
144
147
|
"lz-string": "^1.5.0",
|
|
@@ -172,6 +175,7 @@
|
|
|
172
175
|
"sitemap": "^8.0.0",
|
|
173
176
|
"sonner": "^1.5.0",
|
|
174
177
|
"states-us": "^1.1.1",
|
|
178
|
+
"stepts": "^0.0.1",
|
|
175
179
|
"svgo": "^4.0.0",
|
|
176
180
|
"tailwind-merge": "^2.5.2",
|
|
177
181
|
"tailwindcss": "^3.4.13",
|
|
@@ -192,8 +196,6 @@
|
|
|
192
196
|
"wouter": "^3.3.5",
|
|
193
197
|
"zod": "^3.23.8",
|
|
194
198
|
"zustand": "^4.5.5",
|
|
195
|
-
"zustand-hoist": "^2.0.1"
|
|
196
|
-
"circuit-json-to-kicad": "^0.0.3",
|
|
197
|
-
"kicadts": "^0.0.9"
|
|
199
|
+
"zustand-hoist": "^2.0.1"
|
|
198
200
|
}
|
|
199
201
|
}
|
package/src/App.tsx
CHANGED
|
@@ -87,9 +87,7 @@ const ReleasePreviewPage = lazyImport(() => import("@/pages/preview-release"))
|
|
|
87
87
|
const OrganizationSettingsPage = lazyImport(
|
|
88
88
|
() => import("@/pages/organization-settings"),
|
|
89
89
|
)
|
|
90
|
-
const
|
|
91
|
-
() => import("@/pages/settings-redirect"),
|
|
92
|
-
)
|
|
90
|
+
const UserSettingsPage = lazyImport(() => import("@/pages/user-settings"))
|
|
93
91
|
|
|
94
92
|
class ErrorBoundary extends React.Component<
|
|
95
93
|
{ children: React.ReactNode },
|
|
@@ -270,8 +268,8 @@ function App() {
|
|
|
270
268
|
{/* Organization creation route */}
|
|
271
269
|
<Route path="/orgs/new" component={CreateOrganizationPage} />
|
|
272
270
|
|
|
273
|
-
{/* User settings
|
|
274
|
-
<Route path="/settings" component={
|
|
271
|
+
{/* User settings */}
|
|
272
|
+
<Route path="/settings" component={UserSettingsPage} />
|
|
275
273
|
|
|
276
274
|
{/* Organization settings route */}
|
|
277
275
|
<Route
|
|
@@ -25,6 +25,7 @@ import { CubeIcon } from "@radix-ui/react-icons"
|
|
|
25
25
|
import { useState } from "react"
|
|
26
26
|
import { useAxios } from "@/hooks/use-axios"
|
|
27
27
|
import { useCurrentPackageId } from "@/hooks/use-current-package-id"
|
|
28
|
+
import { downloadStepFile } from "@/lib/download-fns/download-step"
|
|
28
29
|
|
|
29
30
|
interface DownloadButtonAndMenuProps {
|
|
30
31
|
className?: string
|
|
@@ -225,7 +226,19 @@ export function DownloadButtonAndMenu({
|
|
|
225
226
|
kicad_*.zip
|
|
226
227
|
</span>
|
|
227
228
|
</DropdownMenuItem>
|
|
228
|
-
|
|
229
|
+
<DropdownMenuItem
|
|
230
|
+
className="text-xs"
|
|
231
|
+
onSelect={async () => {
|
|
232
|
+
const cj = await getCircuitJson()
|
|
233
|
+
downloadStepFile(cj, unscopedName || "step_file")
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<Download className="mr-1 h-3 w-3" />
|
|
237
|
+
<span className="flex-grow mr-6">Step Format</span>
|
|
238
|
+
<span className="text-[0.6rem] bg-emerald-500 opacity-80 text-white font-mono rounded-md px-1 text-center py-0.5 mr-1">
|
|
239
|
+
STEP
|
|
240
|
+
</span>
|
|
241
|
+
</DropdownMenuItem>
|
|
229
242
|
<DropdownMenuItem
|
|
230
243
|
className="text-xs"
|
|
231
244
|
onSelect={async () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
import { Sentry } from "@/lib/sentry"
|
|
3
|
+
|
|
4
|
+
type NotFoundContext = "package" | "package_release"
|
|
5
|
+
|
|
6
|
+
type SentryNotFoundReporterProps = {
|
|
7
|
+
context: NotFoundContext
|
|
8
|
+
slug: string
|
|
9
|
+
status?: number
|
|
10
|
+
message?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SentryNotFoundReporter = ({
|
|
14
|
+
context,
|
|
15
|
+
slug,
|
|
16
|
+
status,
|
|
17
|
+
message,
|
|
18
|
+
}: SentryNotFoundReporterProps) => {
|
|
19
|
+
const hasLoggedRef = useRef(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (typeof window === "undefined") return
|
|
23
|
+
if (typeof slug !== "string" || slug.length === 0) return
|
|
24
|
+
if (typeof status === "number" && status !== 404) return
|
|
25
|
+
if (hasLoggedRef.current) return
|
|
26
|
+
|
|
27
|
+
hasLoggedRef.current = true
|
|
28
|
+
|
|
29
|
+
Sentry.captureMessage(`package:view:${context}-not-found`, {
|
|
30
|
+
level: "warning",
|
|
31
|
+
tags: {
|
|
32
|
+
slug,
|
|
33
|
+
},
|
|
34
|
+
extra: {
|
|
35
|
+
status,
|
|
36
|
+
message,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}, [context, slug, status, message])
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default SentryNotFoundReporter
|
|
@@ -72,12 +72,14 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
|
|
|
72
72
|
const handleSettingsClick = (e: React.MouseEvent) => {
|
|
73
73
|
e.preventDefault()
|
|
74
74
|
e.stopPropagation()
|
|
75
|
-
window.location.href =
|
|
75
|
+
window.location.href = organization.is_personal_org
|
|
76
|
+
? `/settings`
|
|
77
|
+
: `/${organization.name}/settings`
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
const cardContent = (
|
|
79
81
|
<div
|
|
80
|
-
className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
|
|
82
|
+
className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 h-full ${className}`}
|
|
81
83
|
onClick={handleCardClick}
|
|
82
84
|
>
|
|
83
85
|
<div className="flex items-start gap-4">
|
|
@@ -193,7 +195,7 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
|
|
|
193
195
|
<Link
|
|
194
196
|
key={organization.org_id}
|
|
195
197
|
href={`/${organization.name}`}
|
|
196
|
-
className="block"
|
|
198
|
+
className="block h-full"
|
|
197
199
|
>
|
|
198
200
|
{cardContent}
|
|
199
201
|
</Link>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
3
|
+
|
|
4
|
+
export const useHydration = () => {
|
|
5
|
+
const [hasHydrated, setHasHydrated] = useState(() => {
|
|
6
|
+
if (typeof window === "undefined") return false
|
|
7
|
+
return useGlobalStore.persist?.hasHydrated?.() ?? false
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof window === "undefined") return
|
|
12
|
+
|
|
13
|
+
if (useGlobalStore.persist?.hasHydrated?.()) {
|
|
14
|
+
setHasHydrated(true)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const unsubFinishHydration = useGlobalStore.persist?.onFinishHydration?.(
|
|
19
|
+
() => {
|
|
20
|
+
setHasHydrated(true)
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
unsubFinishHydration?.()
|
|
26
|
+
}
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
return hasHydrated
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ import { saveAs } from "file-saver"
|
|
|
2
2
|
import {
|
|
3
3
|
CircuitJsonToKicadPcbConverter,
|
|
4
4
|
CircuitJsonToKicadSchConverter,
|
|
5
|
+
CircuitJsonToKicadProConverter,
|
|
5
6
|
} from "circuit-json-to-kicad"
|
|
6
7
|
import { AnyCircuitElement } from "circuit-json"
|
|
7
8
|
import JSZip from "jszip"
|
|
@@ -18,9 +19,18 @@ export const downloadKicadFiles = (
|
|
|
18
19
|
schConverter.runUntilFinished()
|
|
19
20
|
const kicadSchContent = schConverter.getOutputString()
|
|
20
21
|
|
|
22
|
+
const proConverter = new CircuitJsonToKicadProConverter(circuitJson, {
|
|
23
|
+
projectName: fileName,
|
|
24
|
+
schematicFilename: `${fileName}.kicad_sch`,
|
|
25
|
+
pcbFilename: `${fileName}.kicad_pcb`,
|
|
26
|
+
})
|
|
27
|
+
proConverter.runUntilFinished()
|
|
28
|
+
const kicadProContent = proConverter.getOutputString()
|
|
29
|
+
|
|
21
30
|
const zip = new JSZip()
|
|
22
31
|
zip.file(`${fileName}.kicad_pcb`, kicadPcbContent)
|
|
23
32
|
zip.file(`${fileName}.kicad_sch`, kicadSchContent)
|
|
33
|
+
zip.file(`${fileName}.kicad_pro`, kicadProContent)
|
|
24
34
|
|
|
25
35
|
zip.generateAsync({ type: "blob" }).then((content) => {
|
|
26
36
|
saveAs(content, `${fileName}_kicad.zip`)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AnyCircuitElement } from "circuit-json"
|
|
2
|
+
import { saveAs } from "file-saver"
|
|
3
|
+
import { circuitJsonToStep } from "circuit-json-to-step"
|
|
4
|
+
|
|
5
|
+
export const downloadStepFile = async (
|
|
6
|
+
circuitJson: AnyCircuitElement[],
|
|
7
|
+
fileName: string,
|
|
8
|
+
) => {
|
|
9
|
+
const content = await circuitJsonToStep(circuitJson)
|
|
10
|
+
const blob = new Blob([content], { type: "text/plain" })
|
|
11
|
+
saveAs(blob, fileName + ".step")
|
|
12
|
+
}
|
|
@@ -63,6 +63,7 @@ export const CreateOrganizationPage = () => {
|
|
|
63
63
|
console.error("Failed to create organization:", error)
|
|
64
64
|
toast.error(
|
|
65
65
|
error?.response?.data?.error?.message ||
|
|
66
|
+
error?.data?.error?.message ||
|
|
66
67
|
"Failed to create organization. Please try again.",
|
|
67
68
|
)
|
|
68
69
|
setIsLoading(false)
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
import { Input } from "@/components/ui/input"
|
|
17
17
|
import { Button } from "@/components/ui/button"
|
|
18
18
|
import { Badge } from "@/components/ui/badge"
|
|
19
|
-
import { Separator } from "@/components/ui/separator"
|
|
20
19
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
21
20
|
import {
|
|
22
21
|
AlertDialog,
|
|
@@ -169,6 +168,10 @@ export default function OrganizationSettingsPage() {
|
|
|
169
168
|
organization.user_permissions?.can_manage_org ||
|
|
170
169
|
organization.owner_account_id === session?.account_id
|
|
171
170
|
|
|
171
|
+
if (organization.is_personal_org) {
|
|
172
|
+
return <Redirect to="/settings" />
|
|
173
|
+
}
|
|
174
|
+
|
|
172
175
|
if (!canManageOrg) {
|
|
173
176
|
return (
|
|
174
177
|
<div className="min-h-screen bg-white">
|
package/src/pages/search.tsx
CHANGED
|
@@ -28,7 +28,8 @@ interface ScoredPackage extends Package {
|
|
|
28
28
|
matches: number[]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
interface ScoredAccount
|
|
31
|
+
interface ScoredAccount
|
|
32
|
+
extends Omit<Account, "account_id" | "is_tscircuit_staff"> {
|
|
32
33
|
score: number
|
|
33
34
|
matches: number[]
|
|
34
35
|
}
|
|
@@ -252,7 +253,11 @@ export const SearchPage = () => {
|
|
|
252
253
|
) : accountSearchResults.length > 0 ? (
|
|
253
254
|
<div className="grid grid-cols-1 w-full sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
|
|
254
255
|
{accountSearchResults.map((account, i) => (
|
|
255
|
-
<UserCard
|
|
256
|
+
<UserCard
|
|
257
|
+
key={i}
|
|
258
|
+
account={account as unknown as Account}
|
|
259
|
+
className="w-full"
|
|
260
|
+
/>
|
|
256
261
|
))}
|
|
257
262
|
</div>
|
|
258
263
|
) : (
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { useLocation, Redirect } from "wouter"
|
|
3
|
+
import { Helmet } from "react-helmet-async"
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
6
|
+
import {
|
|
7
|
+
AlertDialog,
|
|
8
|
+
AlertDialogAction,
|
|
9
|
+
AlertDialogCancel,
|
|
10
|
+
AlertDialogContent,
|
|
11
|
+
AlertDialogDescription,
|
|
12
|
+
AlertDialogFooter,
|
|
13
|
+
AlertDialogHeader,
|
|
14
|
+
AlertDialogTitle,
|
|
15
|
+
} from "@/components/ui/alert-dialog"
|
|
16
|
+
import { useToast } from "@/hooks/use-toast"
|
|
17
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
18
|
+
import { useHydration } from "@/hooks/use-hydration"
|
|
19
|
+
import { AlertTriangle, Loader2, Trash2 } from "lucide-react"
|
|
20
|
+
import Header from "@/components/Header"
|
|
21
|
+
import Footer from "@/components/Footer"
|
|
22
|
+
import { FullPageLoader } from "@/App"
|
|
23
|
+
|
|
24
|
+
export default function UserSettingsPage() {
|
|
25
|
+
const [, navigate] = useLocation()
|
|
26
|
+
const { toast } = useToast()
|
|
27
|
+
const session = useGlobalStore((s) => s.session)
|
|
28
|
+
const hasHydrated = useHydration()
|
|
29
|
+
|
|
30
|
+
const [showDeleteAccountDialog, setShowDeleteAccountDialog] = useState(false)
|
|
31
|
+
|
|
32
|
+
if (!hasHydrated) {
|
|
33
|
+
return <FullPageLoader />
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
return <Redirect to="/" />
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pageTitle = "User Settings - tscircuit"
|
|
41
|
+
|
|
42
|
+
const handleDeleteAccount = () => {
|
|
43
|
+
setShowDeleteAccountDialog(true)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const confirmDeleteAccount = () => {
|
|
47
|
+
// TODO: Implement delete account functionality
|
|
48
|
+
toast({
|
|
49
|
+
title: "Account deleted",
|
|
50
|
+
description: "Your account has been permanently deleted.",
|
|
51
|
+
})
|
|
52
|
+
navigate("/")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="min-h-screen bg-white">
|
|
57
|
+
<Helmet>
|
|
58
|
+
<title>{pageTitle}</title>
|
|
59
|
+
</Helmet>
|
|
60
|
+
<Header />
|
|
61
|
+
|
|
62
|
+
<section className="w-full px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16">
|
|
63
|
+
<div className="max-w-7xl mx-auto py-8">
|
|
64
|
+
<div className="mb-8">
|
|
65
|
+
<div className="flex items-center gap-4 mb-6">
|
|
66
|
+
<Avatar className="h-16 w-16 border-2 border-gray-200 shadow-sm">
|
|
67
|
+
<AvatarImage
|
|
68
|
+
src={`https://github.com/${session.github_username}.png`}
|
|
69
|
+
alt={`${session.github_username} avatar`}
|
|
70
|
+
/>
|
|
71
|
+
<AvatarFallback className="text-lg bg-gradient-to-br from-blue-100 to-indigo-100 text-blue-700 font-medium">
|
|
72
|
+
{(session.github_username || session.account_id || "")
|
|
73
|
+
.slice(0, 2)
|
|
74
|
+
.toUpperCase()}
|
|
75
|
+
</AvatarFallback>
|
|
76
|
+
</Avatar>
|
|
77
|
+
<div>
|
|
78
|
+
<h1 className="text-3xl font-bold text-gray-900">
|
|
79
|
+
Account Settings
|
|
80
|
+
</h1>
|
|
81
|
+
<p className="text-gray-600 mt-1">
|
|
82
|
+
Manage your account preferences and settings
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="space-y-8">
|
|
89
|
+
<div className="bg-white border border-red-200 rounded-xl shadow-sm">
|
|
90
|
+
<div className="px-6 py-5 border-b border-red-200 bg-red-50 rounded-t-xl">
|
|
91
|
+
<h2 className="text-xl font-semibold text-red-900">
|
|
92
|
+
Danger Zone
|
|
93
|
+
</h2>
|
|
94
|
+
<p className="text-sm text-red-600 mt-2">
|
|
95
|
+
Irreversible and destructive actions for your account.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div className="p-6 lg:p-8">
|
|
100
|
+
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
|
|
101
|
+
<div className="flex-1">
|
|
102
|
+
<h3 className="text-sm font-semibold text-gray-900 mb-2">
|
|
103
|
+
Delete Account
|
|
104
|
+
</h3>
|
|
105
|
+
<p className="text-sm text-gray-500 leading-relaxed">
|
|
106
|
+
Permanently delete your account and all associated data.
|
|
107
|
+
This action cannot be undone and will remove all your
|
|
108
|
+
packages, snippets, and account information.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex-shrink-0">
|
|
112
|
+
<Button
|
|
113
|
+
variant="destructive"
|
|
114
|
+
onClick={handleDeleteAccount}
|
|
115
|
+
className="bg-red-600 hover:bg-red-700 text-white px-6 py-2.5 text-sm font-medium shadow-sm w-full lg:w-auto"
|
|
116
|
+
>
|
|
117
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
118
|
+
Delete Account
|
|
119
|
+
</Button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
|
|
128
|
+
<Footer />
|
|
129
|
+
|
|
130
|
+
<AlertDialog
|
|
131
|
+
open={showDeleteAccountDialog}
|
|
132
|
+
onOpenChange={setShowDeleteAccountDialog}
|
|
133
|
+
>
|
|
134
|
+
<AlertDialogContent>
|
|
135
|
+
<AlertDialogHeader>
|
|
136
|
+
<AlertDialogTitle className="flex items-center gap-2 text-red-600">
|
|
137
|
+
<AlertTriangle className="h-5 w-5" />
|
|
138
|
+
Delete Account
|
|
139
|
+
</AlertDialogTitle>
|
|
140
|
+
<AlertDialogDescription>
|
|
141
|
+
Are you absolutely sure you want to delete your account? This
|
|
142
|
+
action is permanent and cannot be undone. All your packages,
|
|
143
|
+
snippets, and account data will be permanently removed.
|
|
144
|
+
</AlertDialogDescription>
|
|
145
|
+
</AlertDialogHeader>
|
|
146
|
+
<AlertDialogFooter>
|
|
147
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
148
|
+
<AlertDialogAction
|
|
149
|
+
onClick={confirmDeleteAccount}
|
|
150
|
+
disabled={true}
|
|
151
|
+
className="bg-red-600 hover:bg-red-700"
|
|
152
|
+
>
|
|
153
|
+
{false && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
154
|
+
Delete Account
|
|
155
|
+
</AlertDialogAction>
|
|
156
|
+
</AlertDialogFooter>
|
|
157
|
+
</AlertDialogContent>
|
|
158
|
+
</AlertDialog>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -3,9 +3,9 @@ import { usePackageFiles } from "@/hooks/use-package-files"
|
|
|
3
3
|
import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
|
|
4
4
|
import { useLocation, useParams } from "wouter"
|
|
5
5
|
import { Helmet } from "react-helmet-async"
|
|
6
|
-
import { useEffect, useState } from "react"
|
|
7
6
|
import NotFoundPage from "./404"
|
|
8
7
|
import { usePackageByName } from "@/hooks/use-package-by-package-name"
|
|
8
|
+
import { SentryNotFoundReporter } from "@/components/SentryNotFoundReporter"
|
|
9
9
|
|
|
10
10
|
export const ViewPackagePage = () => {
|
|
11
11
|
const { author, packageName } = useParams()
|
|
@@ -32,11 +32,31 @@ export const ViewPackagePage = () => {
|
|
|
32
32
|
usePackageFiles(packageRelease?.package_release_id)
|
|
33
33
|
|
|
34
34
|
if (!isLoadingPackage && packageError) {
|
|
35
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<SentryNotFoundReporter
|
|
38
|
+
context="package"
|
|
39
|
+
slug={packageNameFull}
|
|
40
|
+
status={(packageError as any)?.status}
|
|
41
|
+
message={packageError.message}
|
|
42
|
+
/>
|
|
43
|
+
<NotFoundPage heading="Package Not Found" />
|
|
44
|
+
</>
|
|
45
|
+
)
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
if (!isLoadingPackageRelease && packageReleaseError?.status == 404) {
|
|
39
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<SentryNotFoundReporter
|
|
52
|
+
context="package_release"
|
|
53
|
+
slug={packageNameFull}
|
|
54
|
+
status={packageReleaseError.status}
|
|
55
|
+
message={packageReleaseError.message}
|
|
56
|
+
/>
|
|
57
|
+
<NotFoundPage heading="Package Not Found" />
|
|
58
|
+
</>
|
|
59
|
+
)
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
return (
|