├── .commitlintrc ├── .editorconfig ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── apps └── web │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierignore │ ├── README.md │ ├── app │ ├── components │ │ ├── NotFound.tsx │ │ ├── code-highlighter.ts │ │ ├── copy-to-clipboard.tsx │ │ ├── data-grid │ │ │ └── index.tsx │ │ ├── editor │ │ │ ├── assets │ │ │ │ ├── github-dark.json │ │ │ │ └── github-light.json │ │ │ ├── components │ │ │ │ └── monaco.tsx │ │ │ ├── config.ts │ │ │ ├── index.tsx │ │ │ └── utils │ │ │ │ ├── actions.ts │ │ │ │ ├── formatter.ts │ │ │ │ ├── line-selection.tsx │ │ │ │ ├── monarch.ts │ │ │ │ ├── share.ts │ │ │ │ └── theme.ts │ │ ├── error.tsx │ │ ├── global-loader.tsx │ │ ├── lazy-shiki.tsx │ │ ├── monaco │ │ │ ├── _inspiration │ │ │ │ ├── lilac.ts │ │ │ │ └── readme.md │ │ │ ├── helpers │ │ │ │ ├── autocomplete.tsx │ │ │ │ ├── format.ts │ │ │ │ ├── formatter.tsx │ │ │ │ ├── pgsql.ts │ │ │ │ ├── save.ts │ │ │ │ ├── selection.ts │ │ │ │ └── utils.ts │ │ │ ├── index.tsx │ │ │ ├── lint │ │ │ │ └── schema.ts │ │ │ ├── parts │ │ │ │ └── command-formatter.ts │ │ │ ├── readme.md │ │ │ ├── suggestions │ │ │ │ └── index.ts │ │ │ ├── syntax │ │ │ │ └── index.ts │ │ │ └── tips │ │ │ │ └── readme.md │ │ ├── navbar.tsx │ │ ├── paginator.tsx │ │ ├── panel-handle.tsx │ │ ├── pill.tsx │ │ ├── plot │ │ │ ├── components │ │ │ │ └── settings.tsx │ │ │ ├── context │ │ │ │ ├── context.tsx │ │ │ │ ├── provider.tsx │ │ │ │ ├── types.ts │ │ │ │ └── useChart.tsx │ │ │ └── index.tsx │ │ ├── table │ │ │ ├── help.ts │ │ │ └── table.tsx │ │ ├── tag.tsx │ │ ├── tailwind-indicator.tsx │ │ ├── theme-toggle.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── alert-dialog.tsx │ │ │ ├── alert.tsx │ │ │ ├── aspect-ratio.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge │ │ │ │ ├── badge.tsx │ │ │ │ ├── index.ts │ │ │ │ └── variants.tsx │ │ │ ├── button │ │ │ │ ├── button.tsx │ │ │ │ ├── index.ts │ │ │ │ └── variants.tsx │ │ │ ├── calendar.tsx │ │ │ ├── card.tsx │ │ │ ├── carousel.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── combobox.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── drawer.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form │ │ │ │ ├── context.tsx │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ └── useFormField.tsx │ │ │ ├── hover-card.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── link │ │ │ │ ├── index.ts │ │ │ │ └── link.tsx │ │ │ ├── menubar.tsx │ │ │ ├── multi-select.tsx │ │ │ ├── navigation-menu │ │ │ │ ├── index.ts │ │ │ │ ├── navigation-menu.tsx │ │ │ │ └── trigger-style.tsx │ │ │ ├── pagination.tsx │ │ │ ├── pill │ │ │ │ ├── index.ts │ │ │ │ ├── pill.tsx │ │ │ │ └── variants.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── radio-group.tsx │ │ │ ├── resizeable.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── slider.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── toggle-group.tsx │ │ │ ├── toggle │ │ │ │ ├── index.ts │ │ │ │ ├── toggle.tsx │ │ │ │ └── variants.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ │ └── virtualized-grid │ │ │ └── index.tsx │ ├── constants.client.ts │ ├── constants.ts │ ├── context │ │ ├── db │ │ │ ├── context.tsx │ │ │ ├── hooks │ │ │ │ └── useExport.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── useDB.tsx │ │ ├── editor-settings │ │ │ ├── context.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── useEditor.tsx │ │ ├── editor │ │ │ ├── context.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── useEditor.tsx │ │ ├── pagination │ │ │ ├── context.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── usePagination.tsx │ │ ├── panel │ │ │ ├── context.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── usePanel.tsx │ │ ├── query │ │ │ ├── context.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ └── useQuery.tsx │ │ └── session │ │ │ ├── context.tsx │ │ │ ├── data │ │ │ └── newfile-content.ts │ │ │ ├── hooks │ │ │ └── useAddFile.tsx │ │ │ │ └── index.tsx │ │ │ ├── provider.tsx │ │ │ ├── types.ts │ │ │ ├── useSession.tsx │ │ │ └── worker.ts │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── hooks │ │ ├── use-abortable.tsx │ │ ├── use-breakpoints.ts │ │ ├── use-copy-to-clipboard.tsx │ │ ├── use-enter-submit.tsx │ │ ├── use-media-query.tsx │ │ ├── use-mutation-selector.ts │ │ └── use-prettier-worker.tsx │ ├── lib │ │ └── utils.ts │ ├── modules │ │ └── duckdb-singleton.ts │ ├── root.tsx │ ├── routes │ │ ├── $.tsx │ │ ├── _index │ │ │ ├── components │ │ │ │ ├── about.tsx │ │ │ │ ├── editor-panel │ │ │ │ │ ├── components │ │ │ │ │ │ └── open-files.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── navbar.tsx │ │ │ │ ├── playground.tsx │ │ │ │ ├── query-toolbar.tsx │ │ │ │ ├── result-viewer │ │ │ │ │ ├── components │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ ├── dataset-actions.tsx │ │ │ │ │ │ ├── empty.tsx │ │ │ │ │ │ ├── json-viewer │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ └── table.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── settings.tsx │ │ │ │ └── sidepanel │ │ │ │ │ ├── components │ │ │ │ │ ├── data-sources.tsx │ │ │ │ │ ├── editor-files.tsx │ │ │ │ │ ├── query-history.tsx │ │ │ │ │ └── wrapper │ │ │ │ │ │ ├── context │ │ │ │ │ │ ├── context.tsx │ │ │ │ │ │ ├── provider.tsx │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── useWrapper.tsx │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── route.tsx │ │ │ └── workers │ │ │ │ └── is-supported.worker.ts │ │ ├── action.set-theme.ts │ │ └── error.tsx │ ├── sessions.server.ts │ ├── styles │ │ ├── dockview.css │ │ └── globals.css │ ├── types │ │ ├── files │ │ │ ├── code-source.ts │ │ │ └── dataset.ts │ │ └── query.ts │ └── utils │ │ ├── arrow │ │ └── helpers.ts │ │ ├── consume-readable.ts │ │ ├── debounce.ts │ │ ├── duckdb │ │ ├── autocomplete.ts │ │ ├── helpers │ │ │ ├── columnMapper.ts │ │ │ ├── describeColumns.ts │ │ │ ├── getColumnType.ts │ │ │ └── insert-snippet.ts │ │ ├── snippets.ts │ │ └── tokens.ts │ │ ├── env.server.ts │ │ ├── inflect.ts │ │ ├── nonce-provider.ts │ │ ├── platform.ts │ │ ├── sha256.ts │ │ ├── share.ts │ │ ├── splitter.tsx │ │ ├── sql_fmt.ts │ │ ├── sqlfluff │ │ ├── fluff.ts │ │ ├── lint.ts │ │ └── worker.ts │ │ ├── text.ts │ │ ├── timeout.ts │ │ └── transformer.tsx │ ├── env.d.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.js │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVariableVF.woff2 │ │ ├── JetBrainsMono-Italic[wght].woff2 │ │ └── JetBrainsMono[wght].woff2 │ ├── icon512_maskable.png │ ├── icon512_rounded.png │ ├── logo-big.webp │ ├── logo-tiny.webp │ ├── logo.webp │ ├── mstile-150x150.png │ ├── robots.txt │ ├── safari-pinned-tab.svg │ ├── screenshot.jpg │ ├── site.webmanifest │ ├── sitemap.xml │ └── spaceman.webp │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── biome.json ├── commitlint.config.cjs ├── ideas ├── README.MD ├── focus-manager.ts ├── online-manager.ts ├── opfs.ts ├── service-worker.ts ├── sqlmesh │ └── editor.tsx └── subscribable.ts ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | # Debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | pnpm-debug.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | 40 | .sst 41 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Matthew Fainman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | SESSION_SECRET=your_secret_key 3 | -------------------------------------------------------------------------------- /apps/web/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/**/node_modules 3 | **/**/.next 4 | **/**/public 5 | 6 | # Ignore specific files 7 | dist/ 8 | coverage/ 9 | 10 | # turbo 11 | .turbo 12 | .vercel 13 | .public 14 | .build 15 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | .vercel 7 | .env.* 8 | !.env.example 9 | .DS_Store 10 | 11 | # Sentry Config File 12 | .sentryclirc 13 | 14 | # Sentry Config File 15 | .env.sentry-build-plugin 16 | -------------------------------------------------------------------------------- /apps/web/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix + Vite! 2 | 3 | 📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features. 4 | 5 | ## Development 6 | 7 | Run the Express server with Vite dev middleware: 8 | 9 | ```shellscript 10 | npm run dev 11 | ``` 12 | 13 | ## Deployment 14 | 15 | First, build your app for production: 16 | 17 | ```sh 18 | npm run build 19 | ``` 20 | 21 | Then run the app in production mode: 22 | 23 | ```sh 24 | npm start 25 | ``` 26 | 27 | Now you'll need to pick a host to deploy it to. 28 | 29 | ### DIY 30 | 31 | If you're familiar with deploying Express applications you should be right at home. Just make sure to deploy the output of `npm run build` 32 | 33 | - `build/server` 34 | - `build/client` 35 | -------------------------------------------------------------------------------- /apps/web/app/components/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react"; 2 | import { Suspense } from "react"; 3 | 4 | export default function NotFound() { 5 | return ( 6 |
7 |
8 |

9 | 404 10 |

11 |

12 | Page not found 13 |

14 |

15 | {`Sorry, we couldn't find the page you're looking for.`} 16 |

17 |
18 | 22 | Back to home 23 | 24 |
25 |
26 |
27 | 28 | 33 | 34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/app/components/code-highlighter.ts: -------------------------------------------------------------------------------- 1 | import type { HighlighterCore } from "shiki/core"; 2 | import { getHighlighterCore } from "shiki/core"; 3 | import getWasm from "shiki/wasm"; 4 | 5 | let shiki: HighlighterCore | undefined; 6 | 7 | export const getHighlighter = async () => { 8 | if (shiki) return shiki; 9 | 10 | shiki = await getHighlighterCore({ 11 | langs: [import("shiki/langs/sql.mjs"), import("shiki/langs/json.mjs")], 12 | loadWasm: getWasm, 13 | themes: [ 14 | import("shiki/themes/github-light.mjs"), 15 | import("shiki/themes/aurora-x.mjs"), 16 | import("shiki/themes/vitesse-dark.mjs"), 17 | import("shiki/themes/vitesse-light.mjs"), 18 | ], 19 | }); 20 | 21 | return shiki; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/app/components/data-grid/index.tsx: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | import { useMemo } from "react"; 3 | import { 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableHead, 8 | TableHeader, 9 | TableRow, 10 | } from "~/components/ui/table"; 11 | import { usePagination } from "~/context/pagination/usePagination"; 12 | import { type QueryResponse } from "~/types/query"; 13 | import { getArrowTableSchema } from "~/utils/arrow/helpers"; 14 | import { getColumnType } from "~/utils/duckdb/helpers/getColumnType"; 15 | 16 | export default function DataGrid(props: QueryResponse) { 17 | const { limit, offset } = usePagination(); 18 | const { schema, rows } = useMemo(() => { 19 | if (!props.table || props.table.numRows === 0) 20 | return { schema: [], rows: [] }; 21 | const rows = props.table 22 | .slice(offset, offset + limit) 23 | .toArray() 24 | .map((row) => row.toJSON()); 25 | const schema = getArrowTableSchema(props.table); 26 | return { schema, rows }; 27 | }, [props, limit, offset]); 28 | 29 | return ( 30 | 31 | 32 | 33 | {schema.map((column) => { 34 | return {column.name}; 35 | })} 36 | 37 | 38 | 39 | 40 | {rows.map((row, i) => ( 41 | 42 | {Object.entries(row).map(([column, value]) => { 43 | const type = 44 | schema.find((col) => col.name === column)?.type ?? "other"; 45 | 46 | const coercedType = getColumnType(type); 47 | 48 | const Node = dynamicTypeViewer({ 49 | type: coercedType, 50 | value, 51 | }); 52 | 53 | return {Node}; 54 | })} 55 | 56 | ))} 57 | 58 |
59 | ); 60 | } 61 | 62 | type DynamicTypeViewerProps = { 63 | type: 64 | | "bigint" 65 | | "number" 66 | | "integer" 67 | | "boolean" 68 | | "date" 69 | | "string" 70 | | "other"; 71 | value: unknown; 72 | }; 73 | 74 | function dynamicTypeViewer(props: DynamicTypeViewerProps) { 75 | const { type, value } = props; 76 | 77 | switch (type) { 78 | case "date": { 79 | const date = format(new Date(value as string), "PPpp"); 80 | return date; 81 | } 82 | case "string": { 83 | return value as string; 84 | } 85 | case "bigint": { 86 | return (value as bigint).toString(); 87 | } 88 | case "boolean": { 89 | return `${value}` as string; 90 | } 91 | case "other": { 92 | return JSON.stringify(value); 93 | } 94 | case "integer": 95 | case "number": { 96 | if (isNaN(value as number)) return ""; 97 | // round to 2 decimal places 98 | const formatter = new Intl.NumberFormat("en-UK", { 99 | maximumFractionDigits: 2, 100 | }); 101 | return ( 102 | {formatter.format(value as number)} 103 | ); 104 | } 105 | default: 106 | return ""; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /apps/web/app/components/editor/components/monaco.tsx: -------------------------------------------------------------------------------- 1 | // import { Editor, type OnChange, type OnMount } from "@monaco-editor/react"; 2 | // import { useCallback } from "react"; 3 | 4 | // type IProps = { 5 | // defaultValue: string; 6 | // onChange: OnChange; 7 | // onSelect: (selection: { hi: number; lo: number }) => void; 8 | // }; 9 | 10 | // export const Input: React.FC = (props) => { 11 | // const { defaultValue, onChange, onSelect } = props; 12 | 13 | // const onMount = useCallback( 14 | // (editor) => { 15 | // editor.createContextKey("share_available", navigator.share !== undefined); 16 | // const module = editor.getModel()!; 17 | 18 | // const { dispose } = editor.onDidChangeCursorSelection((e) => { 19 | // const start = module.getOffsetAt(e.selection.getStartPosition()); 20 | // const end = module.getOffsetAt(e.selection.getEndPosition()); 21 | 22 | // const lo = start + 1; 23 | // const hi = end + 1; 24 | 25 | // onSelect({ lo, hi }); 26 | // }); 27 | 28 | // return dispose; 29 | // }, 30 | // [onSelect], 31 | // ); 32 | 33 | // return ( 34 | // 41 | // ); 42 | // }; 43 | -------------------------------------------------------------------------------- /apps/web/app/components/editor/config.ts: -------------------------------------------------------------------------------- 1 | // import { loader } from "@monaco-editor/react"; 2 | // import { 3 | // copy_as_markdown, 4 | // copy_as_url, 5 | // open_issue, 6 | // share, 7 | // } from "./utils/actions"; 8 | 9 | // loader.init().then((monaco) => { 10 | // copy_as_url(monaco); 11 | // copy_as_markdown(monaco); 12 | // open_issue(monaco); 13 | // share(monaco); 14 | // monaco.editor.addKeybindingRule({ 15 | // keybinding: 16 | // monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyP, 17 | // command: "editor.action.quickCommand", 18 | // }); 19 | // }); 20 | -------------------------------------------------------------------------------- /apps/web/app/components/editor/index.tsx: -------------------------------------------------------------------------------- 1 | // import lz from "lz-string"; 2 | // import { Suspense, useCallback, useEffect, useState } from "react"; 3 | // import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; 4 | // import { Textarea } from "~/components/ui/textarea"; 5 | // import { localStore } from "./utils/share"; 6 | 7 | // const getStoredCode = (location: Location) => { 8 | // let init_code = ""; 9 | // if (location.hash.startsWith("#code/")) { 10 | // const code = location.hash.replace("#code/", "").trim(); 11 | // localStore.code = init_code = lz.decompressFromEncodedURIComponent(code); 12 | // } else { 13 | // init_code = localStore.code; 14 | // } 15 | 16 | // return init_code; 17 | // }; 18 | 19 | // export default function CodeEditor() { 20 | // const [code, setCode] = useState(""); 21 | // const [selection, setSelection] = useState({ lo: 0, hi: 0 }); 22 | 23 | // useEffect(() => { 24 | // setCode(getStoredCode(window.location)); 25 | // }, []); 26 | 27 | // const onChange = useCallback((value?: string) => { 28 | // if (value !== undefined) { 29 | // setCode(value); 30 | // } 31 | // }, []); 32 | 33 | // return ( 34 | // 35 | // 39 | // 44 | //