@tscircuit/fake-snippets 0.0.66 → 0.0.67

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 (73) hide show
  1. package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
  2. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
  3. package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
  4. package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
  5. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
  6. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
  7. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +0 -11
  8. package/bun.lock +15 -17
  9. package/dist/bundle.js +32 -39
  10. package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
  11. package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
  12. package/package.json +4 -3
  13. package/src/App.tsx +0 -7
  14. package/src/ContextProviders.tsx +2 -0
  15. package/src/components/DownloadButtonAndMenu.tsx +1 -4
  16. package/src/components/Footer.tsx +5 -2
  17. package/src/components/HeaderLogin.tsx +37 -54
  18. package/src/components/ImageWithFallback.tsx +37 -0
  19. package/src/components/JLCPCBImportDialog.tsx +43 -24
  20. package/src/components/PackageCard.tsx +2 -2
  21. package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
  22. package/src/components/PackageSearchResults.tsx +87 -0
  23. package/src/components/PackagesList.tsx +3 -3
  24. package/src/components/PageSearchComponent.tsx +9 -9
  25. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
  26. package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -8
  27. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +24 -14
  28. package/src/components/ViewPackagePage/components/package-header.tsx +6 -1
  29. package/src/components/package-port/CodeEditor.tsx +13 -10
  30. package/src/components/package-port/CodeEditorHeader.tsx +1 -1
  31. package/src/components/package-port/EditorNav.tsx +2 -2
  32. package/src/hooks/use-global-store.ts +1 -0
  33. package/src/hooks/use-shiki-highlighter.ts +13 -6
  34. package/src/lib/download-fns/download-gltf.ts +3 -10
  35. package/src/lib/handleManualEditsImport.tsx +1 -1
  36. package/src/lib/types.ts +4 -2
  37. package/src/pages/dashboard.tsx +3 -4
  38. package/src/pages/editor.tsx +20 -14
  39. package/src/pages/latest.tsx +25 -26
  40. package/src/pages/search.tsx +120 -19
  41. package/src/pages/trending.tsx +14 -58
  42. package/src/pages/user-profile.tsx +13 -8
  43. package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
  44. package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
  45. package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
  46. package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
  47. package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
  48. package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
  49. package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
  50. package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
  51. package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
  52. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
  53. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
  54. package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
  55. package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
  56. package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
  57. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
  58. package/src/components/AiChatInterface.tsx +0 -229
  59. package/src/components/CodeAndPreview.tsx +0 -289
  60. package/src/components/CodeEditor.tsx +0 -539
  61. package/src/components/CodeEditorHeader.tsx +0 -135
  62. package/src/components/EditorNav.tsx +0 -502
  63. package/src/components/PreviewContent.tsx +0 -372
  64. package/src/components/SnippetCard.tsx +0 -159
  65. package/src/components/SnippetList.tsx +0 -71
  66. package/src/hooks/use-compiled-tsx.ts +0 -37
  67. package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
  68. package/src/hooks/use-run-tsx/index.tsx +0 -256
  69. package/src/hooks/use-save-snippet.ts +0 -66
  70. package/src/hooks/use-typecheck.ts +0 -54
  71. package/src/lib/utils/getSyntaxError.ts +0 -13
  72. package/src/pages/ai.tsx +0 -92
  73. package/src/pages/view-snippet.tsx +0 -166
@@ -1,539 +0,0 @@
1
- import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
2
- import { basicSetup } from "@/lib/codemirror/basic-setup"
3
- import { autocompletion } from "@codemirror/autocomplete"
4
- import { indentWithTab } from "@codemirror/commands"
5
- import { javascript } from "@codemirror/lang-javascript"
6
- import { json } from "@codemirror/lang-json"
7
- import { EditorState } from "@codemirror/state"
8
- import { Decoration, hoverTooltip, keymap } from "@codemirror/view"
9
- import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
10
- import type { ATABootstrapConfig } from "@typescript/ata"
11
- import { setupTypeAcquisition } from "@typescript/ata"
12
- import { TSCI_PACKAGE_PATTERN } from "../lib/constants"
13
- import {
14
- createDefaultMapFromCDN,
15
- createSystem,
16
- createVirtualTypeScriptEnvironment,
17
- } from "@typescript/vfs"
18
- import {
19
- tsAutocomplete,
20
- tsFacet,
21
- tsHover,
22
- tsLinter,
23
- tsSync,
24
- } from "@valtown/codemirror-ts"
25
- import { EditorView } from "codemirror"
26
- import { useEffect, useMemo, useRef, useState } from "react"
27
- import ts from "typescript"
28
- import CodeEditorHeader from "./CodeEditorHeader"
29
- // import { copilotPlugin, Language } from "@valtown/codemirror-codeium"
30
- import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
31
- const defaultImports = `
32
- import React from "@types/react/jsx-runtime"
33
- import { Circuit, createUseComponent } from "@tscircuit/core"
34
- import type { CommonLayoutProps } from "@tscircuit/props"
35
- `
36
- import { getSingletonHighlighter, Highlighter } from "shiki"
37
- import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
38
-
39
- export const CodeEditor = ({
40
- onCodeChange,
41
- onDtsChange,
42
- readOnly = false,
43
- initialCode = "",
44
- manualEditsFileContent,
45
- isStreaming = false,
46
- showImportAndFormatButtons = true,
47
- onManualEditsFileContentChanged,
48
- }: {
49
- onCodeChange: (code: string, filename?: string) => void
50
- onDtsChange?: (dts: string) => void
51
- initialCode: string
52
- readOnly?: boolean
53
- isStreaming?: boolean
54
- manualEditsFileContent: string
55
- showImportAndFormatButtons?: boolean
56
- onManualEditsFileContentChanged?: (newContent: string) => void
57
- }) => {
58
- const editorRef = useRef<HTMLDivElement>(null)
59
- const viewRef = useRef<EditorView | null>(null)
60
- const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
61
- const apiUrl = useSnippetsBaseApiUrl()
62
- const codeCompletionApi = useCodeCompletionApi()
63
-
64
- const { highlighter, isLoading } = useShikiHighlighter()
65
-
66
- const [cursorPosition, setCursorPosition] = useState<number | null>(null)
67
- const [code, setCode] = useState(initialCode)
68
-
69
- const files = useMemo(
70
- () => ({
71
- "index.tsx": code,
72
- "manual-edits.json": manualEditsFileContent,
73
- }),
74
- [code, manualEditsFileContent],
75
- )
76
- const [currentFile, setCurrentFile] =
77
- useState<keyof typeof files>("index.tsx")
78
-
79
- const isInitialCodeLoaded = Boolean(initialCode)
80
-
81
- useEffect(() => {
82
- if (initialCode !== code) {
83
- setCode(initialCode)
84
- if (currentFile === "index.tsx") {
85
- updateCurrentEditorContent(initialCode)
86
- }
87
- }
88
- }, [isInitialCodeLoaded])
89
-
90
- // Whenever streaming completes, reset the code to the initial code
91
- useEffect(() => {
92
- if (!isStreaming && code !== initialCode && initialCode) {
93
- setCode(initialCode)
94
-
95
- // HACK: Timeout because we need to wait for the editor to mount again
96
- setTimeout(() => {
97
- updateCurrentEditorContent(initialCode)
98
- }, 200)
99
- }
100
- }, [isStreaming])
101
-
102
- useEffect(() => {
103
- if (!editorRef.current) return
104
-
105
- const fsMap = new Map<string, string>()
106
- Object.entries(files).forEach(([filename, content]) => {
107
- fsMap.set(filename, content)
108
- })
109
- ;(window as any).__DEBUG_CODE_EDITOR_FS_MAP = fsMap
110
-
111
- createDefaultMapFromCDN(
112
- { target: ts.ScriptTarget.ES2022 },
113
- "5.6.3",
114
- true,
115
- ts,
116
- ).then((defaultFsMap) => {
117
- defaultFsMap.forEach((content, filename) => {
118
- fsMap.set(filename, content)
119
- })
120
- })
121
-
122
- const system = createSystem(fsMap)
123
- const env = createVirtualTypeScriptEnvironment(system, [], ts, {
124
- jsx: ts.JsxEmit.ReactJSX,
125
- declaration: true,
126
- allowJs: true,
127
- target: ts.ScriptTarget.ES2022,
128
- resolveJsonModule: true,
129
- })
130
-
131
- // Initialize ATA
132
- const ataConfig: ATABootstrapConfig = {
133
- projectName: "my-project",
134
- typescript: ts,
135
- logger: console,
136
- fetcher: async (input: RequestInfo | URL, init?: RequestInit) => {
137
- const registryPrefixes = [
138
- "https://data.jsdelivr.com/v1/package/resolve/npm/@tsci/",
139
- "https://data.jsdelivr.com/v1/package/npm/@tsci/",
140
- "https://cdn.jsdelivr.net/npm/@tsci/",
141
- ]
142
- if (
143
- typeof input === "string" &&
144
- registryPrefixes.some((prefix) => input.startsWith(prefix))
145
- ) {
146
- const fullPackageName = input
147
- .replace(registryPrefixes[0], "")
148
- .replace(registryPrefixes[1], "")
149
- .replace(registryPrefixes[2], "")
150
- const packageName = fullPackageName.split("/")[0].replace(/\./, "/")
151
- const pathInPackage = fullPackageName.split("/").slice(1).join("/")
152
- const jsdelivrPath = `${packageName}${pathInPackage ? `/${pathInPackage}` : ""}`
153
- return fetch(
154
- `${apiUrl}/snippets/download?jsdelivr_resolve=${input.includes("/resolve/")}&jsdelivr_path=${encodeURIComponent(jsdelivrPath)}`,
155
- )
156
- }
157
- return fetch(input, init)
158
- },
159
- delegate: {
160
- started: () => {
161
- const manualEditsTypeDeclaration = `
162
- declare module "*.json" {
163
- const value: {
164
- pcb_placements?: any[],
165
- schematic_placements?: any[],
166
- edit_events?: any[],
167
- manual_trace_hints?: any[],
168
- } | undefined;
169
- export default value;
170
- }
171
- `
172
- env.createFile("manual-edits.d.ts", manualEditsTypeDeclaration)
173
- },
174
- receivedFile: (code: string, path: string) => {
175
- fsMap.set(path, code)
176
- env.createFile(path, code)
177
- if (viewRef.current) {
178
- viewRef.current.dispatch({
179
- changes: {
180
- from: 0,
181
- to: viewRef.current.state.doc.length,
182
- insert: viewRef.current.state.doc.toString(),
183
- },
184
- selection: viewRef.current.state.selection,
185
- })
186
- }
187
- },
188
- },
189
- }
190
-
191
- const ata = setupTypeAcquisition(ataConfig)
192
- ataRef.current = ata
193
-
194
- const lastFilesEventContent: Record<string, string> = {}
195
-
196
- // Set up base extensions
197
- const baseExtensions = [
198
- basicSetup,
199
- currentFile.endsWith(".json")
200
- ? json()
201
- : javascript({ typescript: true, jsx: true }),
202
- keymap.of([indentWithTab]),
203
- EditorState.readOnly.of(readOnly),
204
- EditorView.updateListener.of((update) => {
205
- if (update.docChanged) {
206
- const newContent = update.state.doc.toString()
207
-
208
- if (newContent === lastFilesEventContent[currentFile]) return
209
- lastFilesEventContent[currentFile] = newContent
210
-
211
- if (currentFile === "index.tsx") {
212
- setCode(newContent)
213
- onCodeChange(newContent)
214
- } else if (currentFile === "manual-edits.json") {
215
- onManualEditsFileContentChanged?.(newContent)
216
- }
217
-
218
- if (currentFile === "index.tsx") {
219
- const { outputFiles } = env.languageService.getEmitOutput(
220
- currentFile,
221
- true,
222
- )
223
- const indexDts = outputFiles.find(
224
- (file) => file.name === "index.d.ts",
225
- )
226
- if (indexDts?.text && onDtsChange) {
227
- onDtsChange(indexDts.text)
228
- }
229
- }
230
- }
231
- if (update.selectionSet) {
232
- const pos = update.state.selection.main.head
233
- setCursorPosition(pos)
234
- }
235
- }),
236
- ]
237
- if (codeCompletionApi?.apiKey) {
238
- baseExtensions.push(
239
- // copilotPlugin({
240
- // apiKey: codeCompletionApi.apiKey,
241
- // language: Language.TYPESCRIPT,
242
- // }),
243
- EditorView.theme({
244
- ".cm-ghostText, .cm-ghostText *": {
245
- opacity: "0.6",
246
- filter: "grayscale(20%)",
247
- cursor: "pointer",
248
- },
249
- ".cm-ghostText:hover": {
250
- background: "#eee",
251
- },
252
- ".cm-content": {
253
- cursor: "text",
254
- },
255
- ".cm-underline": {
256
- cursor: "text",
257
- },
258
- ".cm-underline:hover": {
259
- cursor: "pointer",
260
- },
261
- }),
262
- )
263
- }
264
-
265
- // Add TypeScript-specific extensions and handlers
266
- const tsExtensions =
267
- currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")
268
- ? [
269
- tsFacet.of({ env, path: currentFile }),
270
- tsSync(),
271
- tsLinter(),
272
- autocompletion({ override: [tsAutocomplete()] }),
273
- hoverTooltip((view, pos) => {
274
- const line = view.state.doc.lineAt(pos)
275
- const lineStart = line.from
276
- const lineEnd = line.to
277
- const lineText = view.state.sliceDoc(lineStart, lineEnd)
278
- const matches = Array.from(
279
- lineText.matchAll(TSCI_PACKAGE_PATTERN),
280
- )
281
-
282
- for (const match of matches) {
283
- if (match.index !== undefined) {
284
- const start = lineStart + match.index
285
- const end = start + match[0].length
286
- if (pos >= start && pos <= end) {
287
- return {
288
- pos: start,
289
- end: end,
290
- above: true,
291
- create() {
292
- const dom = document.createElement("div")
293
- dom.textContent = "Ctrl/Cmd+Click to open snippet"
294
- return { dom }
295
- },
296
- }
297
- }
298
- }
299
- }
300
-
301
- const facet = view.state.facet(tsFacet)
302
- if (!facet) return null
303
-
304
- const { env, path } = facet
305
- const info = env.languageService.getQuickInfoAtPosition(path, pos)
306
- if (!info) return null
307
-
308
- const start = info.textSpan.start
309
- const end = start + info.textSpan.length
310
- const content = ts.displayPartsToString(info.displayParts || [])
311
-
312
- const dom = document.createElement("div")
313
- if (highlighter) {
314
- dom.innerHTML = highlighter.codeToHtml(content, {
315
- lang: "typescript",
316
- themes: {
317
- light: "github-light",
318
- dark: "github-dark",
319
- },
320
- })
321
-
322
- return {
323
- pos: start,
324
- end,
325
- above: true,
326
- create: () => ({ dom }),
327
- }
328
- }
329
- return null
330
- }),
331
- EditorView.domEventHandlers({
332
- keydown: (event, view) => {
333
- if (event.ctrlKey || event.metaKey) {
334
- const content = view.contentDOM
335
- const underlinedElements =
336
- content.querySelectorAll<HTMLElement>(".cm-underline")
337
- underlinedElements.forEach((el) => {
338
- el.style.cursor = "pointer"
339
- })
340
- }
341
- return false
342
- },
343
- keyup: (event, view) => {
344
- if (event.key === "Control" || event.key === "Meta") {
345
- const content = view.contentDOM
346
- const underlinedElements =
347
- content.querySelectorAll<HTMLElement>(".cm-underline")
348
- underlinedElements.forEach((el) => {
349
- el.style.cursor = "text"
350
- })
351
- }
352
- return false
353
- },
354
- click: (event, view) => {
355
- if (!event.ctrlKey && !event.metaKey) return false
356
- const pos = view.posAtCoords({
357
- x: event.clientX,
358
- y: event.clientY,
359
- })
360
- if (pos === null) return false
361
-
362
- const line = view.state.doc.lineAt(pos)
363
- const lineStart = line.from
364
- const lineEnd = line.to
365
- const lineText = view.state.sliceDoc(lineStart, lineEnd)
366
- const matches = Array.from(
367
- lineText.matchAll(TSCI_PACKAGE_PATTERN),
368
- )
369
- for (const match of matches) {
370
- if (match.index !== undefined) {
371
- const start = lineStart + match.index
372
- const end = start + match[0].length
373
- if (pos >= start && pos <= end) {
374
- const importName = match[0]
375
- // Handle potential dots and dashes in package names
376
- const [owner, name] = importName
377
- .replace("@tsci/", "")
378
- .split(".")
379
- window.open(`/${owner}/${name}`, "_blank")
380
- return true
381
- }
382
- }
383
- }
384
- return false
385
- },
386
- }),
387
- EditorView.theme({
388
- ".shiki": {
389
- maxWidth: "600px",
390
- padding: "12px",
391
- maxHeight: "400px",
392
- borderRadius: "0.5rem",
393
- backgroundColor: "#fff",
394
- color: "#0f172a",
395
- border: "1px solid #e2e8f0",
396
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.08)",
397
- fontSize: "14px",
398
- fontFamily: "monospace",
399
- whiteSpace: "pre-wrap",
400
- lineHeight: "1.6",
401
- overflow: "auto",
402
- zIndex: "9999",
403
- },
404
- }),
405
- EditorView.decorations.of((view) => {
406
- const decorations = []
407
- for (const { from, to } of view.visibleRanges) {
408
- for (let pos = from; pos < to; ) {
409
- const line = view.state.doc.lineAt(pos)
410
- const lineText = line.text
411
- const matches = lineText.matchAll(TSCI_PACKAGE_PATTERN)
412
- for (const match of matches) {
413
- if (match.index !== undefined) {
414
- const start = line.from + match.index
415
- const end = start + match[0].length
416
- decorations.push(
417
- Decoration.mark({
418
- class: "cm-underline",
419
- }).range(start, end),
420
- )
421
- }
422
- }
423
- pos = line.to + 1
424
- }
425
- }
426
- return Decoration.set(decorations)
427
- }),
428
- ]
429
- : []
430
-
431
- const state = EditorState.create({
432
- doc: files[currentFile],
433
- extensions: [...baseExtensions, ...tsExtensions],
434
- })
435
-
436
- const view = new EditorView({
437
- state,
438
- parent: editorRef.current,
439
- })
440
-
441
- viewRef.current = view
442
-
443
- // Initial ATA run for index.tsx
444
- if (currentFile === "index.tsx") {
445
- ata(`${defaultImports}${code}`)
446
- }
447
-
448
- return () => {
449
- view.destroy()
450
- }
451
- }, [!isStreaming, currentFile, code !== ""])
452
-
453
- const updateCurrentEditorContent = (newContent: string) => {
454
- if (viewRef.current) {
455
- const state = viewRef.current.state
456
- if (state.doc.toString() !== newContent) {
457
- viewRef.current.dispatch({
458
- changes: { from: 0, to: state.doc.length, insert: newContent },
459
- })
460
- }
461
- }
462
- }
463
-
464
- const updateEditorToMatchCurrentFile = () => {
465
- const currentContent = files[currentFile] || ""
466
- updateCurrentEditorContent(currentContent)
467
- }
468
-
469
- const codeImports = getImportsFromCode(code)
470
-
471
- useEffect(() => {
472
- if (ataRef.current && currentFile === "index.tsx") {
473
- ataRef.current(`${defaultImports}${code}`)
474
- }
475
- }, [codeImports])
476
-
477
- const handleFileChange = (filename: `${string}.${string}`) => {
478
- setCurrentFile(filename as keyof typeof files)
479
- }
480
-
481
- const updateFileContent = (
482
- filename: keyof typeof files,
483
- newContent: string,
484
- ) => {
485
- if (filename === "index.tsx") {
486
- setCode(newContent)
487
- onCodeChange(newContent)
488
- } else if (filename === "manual-edits.json") {
489
- onManualEditsFileContentChanged?.(newContent)
490
- }
491
-
492
- if (viewRef.current && currentFile === filename) {
493
- viewRef.current.dispatch({
494
- changes: {
495
- from: 0,
496
- to: viewRef.current.state.doc.length,
497
- insert: newContent,
498
- },
499
- })
500
- }
501
- }
502
-
503
- // Whenever the current file changes, updated the editor content
504
- useEffect(() => {
505
- updateEditorToMatchCurrentFile()
506
- }, [currentFile])
507
-
508
- // Whenever the manual edits json content changes, update the editor if
509
- // it's currently viewing the manual edits file
510
- useEffect(() => {
511
- if (currentFile === "manual-edits.json") {
512
- updateEditorToMatchCurrentFile()
513
- }
514
- }, [manualEditsFileContent])
515
-
516
- if (isStreaming) {
517
- return (
518
- <div className="font-mono whitespace-pre-wrap text-xs">{initialCode}</div>
519
- )
520
- }
521
-
522
- return (
523
- <div className="flex flex-col h-full">
524
- {showImportAndFormatButtons && (
525
- <CodeEditorHeader
526
- currentFile={currentFile}
527
- files={files}
528
- handleFileChange={handleFileChange}
529
- updateFileContent={(...args) => {
530
- return updateFileContent(
531
- ...(args as Parameters<typeof updateFileContent>),
532
- )
533
- }}
534
- />
535
- )}
536
- <div ref={editorRef} className="flex-1 overflow-auto" />
537
- </div>
538
- )
539
- }
@@ -1,135 +0,0 @@
1
- import { Button } from "@/components/ui/button"
2
- import {
3
- Select,
4
- SelectContent,
5
- SelectItem,
6
- SelectTrigger,
7
- SelectValue,
8
- } from "@/components/ui/select"
9
- import { useImportSnippetDialog } from "./dialogs/import-snippet-dialog"
10
- import { useToast } from "@/hooks/use-toast"
11
- import {
12
- DropdownMenu,
13
- DropdownMenuContent,
14
- DropdownMenuItem,
15
- DropdownMenuTrigger,
16
- } from "./ui/dropdown-menu"
17
- import { AlertTriangle } from "lucide-react"
18
- import { checkIfManualEditsImported } from "@/lib/utils/checkIfManualEditsImported"
19
- import { handleManualEditsImport } from "@/lib/handleManualEditsImport"
20
-
21
- export type FileName = "index.tsx" | "manual-edits.json" | `${string}.${string}`
22
-
23
- interface CodeEditorHeaderProps {
24
- currentFile: FileName
25
- files: Record<FileName, string>
26
- handleFileChange: (filename: FileName) => void
27
- updateFileContent: (filename: FileName, content: string) => void
28
- }
29
-
30
- export const CodeEditorHeader = ({
31
- currentFile,
32
- files,
33
- handleFileChange,
34
- updateFileContent,
35
- }: CodeEditorHeaderProps) => {
36
- const { Dialog: ImportSnippetDialog, openDialog: openImportDialog } =
37
- useImportSnippetDialog()
38
- const { toast } = useToast()
39
-
40
- const formatCurrentFile = () => {
41
- if (!window.prettier || !window.prettierPlugins) return
42
-
43
- try {
44
- const currentContent = files[currentFile]
45
-
46
- if (currentFile.endsWith(".json")) {
47
- try {
48
- const jsonObj = JSON.parse(currentContent)
49
- const formattedJson = JSON.stringify(jsonObj, null, 2)
50
- updateFileContent(currentFile, formattedJson)
51
- } catch (jsonError) {
52
- throw new Error("Invalid JSON content")
53
- }
54
- return
55
- }
56
-
57
- const formattedCode = window.prettier.format(currentContent, {
58
- semi: false,
59
- parser: "typescript",
60
- plugins: window.prettierPlugins,
61
- })
62
-
63
- updateFileContent(currentFile, formattedCode)
64
- } catch (error) {
65
- console.error("Formatting error:", error)
66
- toast({
67
- title: "Formatting error",
68
- description:
69
- error instanceof Error
70
- ? error.message
71
- : "Failed to format the code. Please check for syntax errors.",
72
- variant: "destructive",
73
- })
74
- }
75
- }
76
-
77
- return (
78
- <div className="flex items-center gap-2 px-2 border-b border-gray-200">
79
- <div>
80
- <Select value={currentFile} onValueChange={handleFileChange}>
81
- <SelectTrigger className="h-7 px-3 bg-white">
82
- <SelectValue placeholder="Select file" />
83
- </SelectTrigger>
84
- <SelectContent>
85
- {Object.keys(files).map((filename) => (
86
- <SelectItem className="py-1" key={filename} value={filename}>
87
- <span className="text-xs pr-1">{filename}</span>
88
- </SelectItem>
89
- ))}
90
- </SelectContent>
91
- </Select>
92
- </div>
93
- <div className="flex items-center gap-2 px-2 py-1 ml-auto">
94
- {checkIfManualEditsImported(files) && (
95
- <DropdownMenu>
96
- <DropdownMenuTrigger asChild>
97
- <Button
98
- size="sm"
99
- variant="ghost"
100
- className="text-red-500 hover:bg-red-50"
101
- >
102
- <AlertTriangle className="mr-2 h-4 w-4" />
103
- Error
104
- </Button>
105
- </DropdownMenuTrigger>
106
- <DropdownMenuContent>
107
- <DropdownMenuItem
108
- className="text-red-600 cursor-pointer"
109
- onClick={() =>
110
- handleManualEditsImport(files, updateFileContent, toast)
111
- }
112
- >
113
- Manual edits exist but have not been imported. (Click to fix)
114
- </DropdownMenuItem>
115
- </DropdownMenuContent>
116
- </DropdownMenu>
117
- )}
118
- <Button size="sm" variant="ghost" onClick={() => openImportDialog()}>
119
- Import
120
- </Button>
121
- <Button size="sm" variant="ghost" onClick={formatCurrentFile}>
122
- Format
123
- </Button>
124
- </div>
125
- <ImportSnippetDialog
126
- onSnippetSelected={(snippet) => {
127
- const newContent = `import {} from "@tsci/${snippet.owner_name}.${snippet.unscoped_name}"\n${files[currentFile]}`
128
- updateFileContent(currentFile, newContent)
129
- }}
130
- />
131
- </div>
132
- )
133
- }
134
-
135
- export default CodeEditorHeader