From 0e034917c202b1f018bac1356ccb062e9579faeb Mon Sep 17 00:00:00 2001 From: Clemens Solar Date: Sat, 20 Jan 2024 23:25:58 +0100 Subject: [PATCH] refactor: get rid of react-router, simplify import --- esbuild.app.mjs | 1 + package-lock.json | 61 ---------- package.json | 1 - src/App.tsx | 79 +++++++++++++ src/{pages => components}/Editor.tsx | 54 +++++---- src/components/ErrorBox.tsx | 24 ++++ src/components/ImportFileSelector.tsx | 45 ++++++++ src/components/Layout.tsx | 17 +-- src/components/Layout/ImportFromUrlDialog.tsx | 108 +++--------------- src/hooks/useImport.tsx | 28 +++++ src/index.tsx | 7 +- src/lib/fetcha.ts | 4 +- src/lib/fetcha/printables.com.ts | 20 ++-- src/pages/Import.tsx | 104 ----------------- src/router.tsx | 35 ------ 15 files changed, 247 insertions(+), 341 deletions(-) create mode 100644 src/App.tsx rename src/{pages => components}/Editor.tsx (87%) create mode 100644 src/components/ErrorBox.tsx create mode 100644 src/components/ImportFileSelector.tsx create mode 100644 src/hooks/useImport.tsx delete mode 100644 src/pages/Import.tsx delete mode 100644 src/router.tsx diff --git a/esbuild.app.mjs b/esbuild.app.mjs index 9d6ce51..b96ac33 100644 --- a/esbuild.app.mjs +++ b/esbuild.app.mjs @@ -31,6 +31,7 @@ export default { __TRACKER_SNIPPET: process.env.TRACKER_SNIPPET || '', __GITHUB_ISSUE_URL: process.env.GH_ISSUE_URL || '', __GITHUB_REPO_URL: process.env.GH_REPO_URL || '', + __WEBSITE_URL: process.env.WEBSITE_URL || 'http://localhost:8000/', }), ], }; diff --git a/package-lock.json b/package-lock.json index a371d78..f6733f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.21.1", "react-stl-viewer": "^2.5.0" }, "devDependencies": { @@ -2456,14 +2455,6 @@ } } }, - "node_modules/@remix-run/router": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", - "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -8550,36 +8541,6 @@ "react": "^18.0.0" } }, - "node_modules/react-router": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.2.tgz", - "integrity": "sha512-jJcgiwDsnaHIeC+IN7atO0XiSRCrOsQAHHbChtJxmgqG2IaYQXSnhqGb5vk2CU/wBQA12Zt+TkbuJjIn65gzbA==", - "dependencies": { - "@remix-run/router": "1.14.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.2.tgz", - "integrity": "sha512-tE13UukgUOh2/sqYr6jPzZTzmzc70aGRP4pAjG2if0IP3aUT+sBtAKUJh0qMh0zylJHGLmzS+XWVaON4UklHeg==", - "dependencies": { - "@remix-run/router": "1.14.2", - "react-router": "6.21.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, "node_modules/react-stl-viewer": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/react-stl-viewer/-/react-stl-viewer-2.5.0.tgz", @@ -11462,11 +11423,6 @@ "zustand": "^3.7.1" } }, - "@remix-run/router": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz", - "integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==" - }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -15995,23 +15951,6 @@ "scheduler": "^0.21.0" } }, - "react-router": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.2.tgz", - "integrity": "sha512-jJcgiwDsnaHIeC+IN7atO0XiSRCrOsQAHHbChtJxmgqG2IaYQXSnhqGb5vk2CU/wBQA12Zt+TkbuJjIn65gzbA==", - "requires": { - "@remix-run/router": "1.14.2" - } - }, - "react-router-dom": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.2.tgz", - "integrity": "sha512-tE13UukgUOh2/sqYr6jPzZTzmzc70aGRP4pAjG2if0IP3aUT+sBtAKUJh0qMh0zylJHGLmzS+XWVaON4UklHeg==", - "requires": { - "@remix-run/router": "1.14.2", - "react-router": "6.21.2" - } - }, "react-stl-viewer": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/react-stl-viewer/-/react-stl-viewer-2.5.0.tgz", diff --git a/package.json b/package.json index bb23e24..af3b9fe 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "notistack": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.21.1", "react-stl-viewer": "^2.5.0" } } diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..91c9222 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,79 @@ +import { Box, CircularProgress, Paper, styled } from '@mui/material'; +import React from 'react'; + +import Editor from './components/Editor'; +import ErrorBox from './components/ErrorBox'; +import ImportFileSelector from './components/ImportFileSelector'; +import useImport from './hooks/useImport'; + +const MyBox = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + position: 'absolute', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + width: '50vw', + maxWidth: '50vw', +})); + +export default function App() { + const importUrl = getImportUrl(); + + const { error, files, isLoading } = useImport(importUrl); + const [selectedIndex, setSelectedIndex] = React.useState(); + let file: string | undefined; + + // Show a loading indicator during the import. + if (isLoading) { + return ( + + + + ); + } + + // Show an error message if the import failed. + if (error) { + return ( + + + + ); + } + + // If there are multiple files, let the user select the one to import. + if (files.length > 1 && selectedIndex === undefined) { + return ( + + + + + + ); + } + + // If there is only one file, we can directly import it. + if (files.length === 1) { + file = files[0].url; + + // If the user has selected a file, we can import it. + } else if (selectedIndex !== undefined && files.length > selectedIndex) { + file = files[selectedIndex].url; + } + + return ; +} + +function getImportUrl(): string | undefined { + let search = window.location.search; + + // Trim the leading question mark + if (search.startsWith('?')) { + search = search.substring(1); + } + + // If the search string is an url, load it through the fetcha. + if (search.startsWith('http')) { + return decodeURIComponent(search); + } +} diff --git a/src/pages/Editor.tsx b/src/components/Editor.tsx similarity index 87% rename from src/pages/Editor.tsx rename to src/components/Editor.tsx index 13c1736..cbf8b7c 100644 --- a/src/pages/Editor.tsx +++ b/src/components/Editor.tsx @@ -7,17 +7,16 @@ import Stack from '@mui/material/Stack'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import React, { useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; -import CodeEditor from '../components/CodeEditor'; -import Customizer from '../components/Customizer'; -import { useOpenSCADProvider } from '../components/OpenscadWorkerProvider'; -import Preview from '../components/Preview'; -import SplitButton from '../components/SplitButton'; import executeOpenSCAD from '../lib/openSCAD/execute'; import parseOpenScadParameters, { Parameter, } from '../lib/openSCAD/parseParameter'; +import CodeEditor from './CodeEditor'; +import Customizer from './Customizer'; +import { useOpenSCADProvider } from './OpenscadWorkerProvider'; +import Preview from './Preview'; +import SplitButton from './SplitButton'; const loopAnimation = { animation: 'spin 2s linear infinite', @@ -31,19 +30,35 @@ const loopAnimation = { }, }; -export default function Editor() { - const { log, preview, previewFile, reset } = useOpenSCADProvider(); - const location = useLocation(); +type Props = { + url?: string; + initialMode?: string; +}; + +export default function Editor({ url, initialMode }: Props) { + const { log, preview, previewFile } = useOpenSCADProvider(); const logRef = React.useRef(null); - const [code, setCode] = React.useState('cube(15, center=true);'); + const [code, setCode] = React.useState(); const [isExporting, setIsExporting] = React.useState(false); const [isRendering, setIsRendering] = React.useState(false); const [mode, setMode] = React.useState( - location.state?.mode || 'editor' + initialMode || 'editor' ); const [parameters, setParameters] = React.useState([]); + useEffect(() => { + if (url) { + (async () => { + const codeResponse = await fetch(url); + const codeBody = await codeResponse.text(); + setCode(codeBody); + await preview!(codeBody, parameters); + setIsRendering(false); + })(); + } + }, [url]); + useEffect(() => { if (previewFile) { setIsRendering(false); @@ -69,21 +84,12 @@ export default function Editor() { } }; - // Load file into text editor - useEffect(() => { - (async () => { - const file = location.state?.file; - - if (file) { - const text = await file.text(); - reset(); - setCode(text); - } - })(); - }, [location]); - // Whenever the code changes, attempt to parse the parameters useEffect(() => { + if (!code) { + return; + } + const newParams = parseOpenScadParameters(code); // Add old values to new params if (parameters.length) { diff --git a/src/components/ErrorBox.tsx b/src/components/ErrorBox.tsx new file mode 100644 index 0000000..0757fb2 --- /dev/null +++ b/src/components/ErrorBox.tsx @@ -0,0 +1,24 @@ +import { Alert, AlertTitle } from '@mui/material'; +import React from 'react'; + +type Props = { + error: Error; +}; + +export default function ErrorBox({ error }: Props) { + return ( + + Error + The following error happened while importing the file. You could try to + reload the page and try again. If the error persists, please report it on{' '} + + GitHub + + .
{error.message}
+
+ Stack trace +
{error.stack}
+
+
+ ); +} diff --git a/src/components/ImportFileSelector.tsx b/src/components/ImportFileSelector.tsx new file mode 100644 index 0000000..8ece54c --- /dev/null +++ b/src/components/ImportFileSelector.tsx @@ -0,0 +1,45 @@ +import FileOpenIcon from '@mui/icons-material/FileOpen'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Avatar from '@mui/material/Avatar'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import React from 'react'; + +import { FetchaFile } from '../lib/fetcha'; + +type Props = { + files: FetchaFile[]; + onSelect: (index: number) => void; +}; + +export default function ImportFileSelector({ files, onSelect }: Props) { + return ( + <> + + Multiple files found at the given location + Please select the one you want to import. + + + {files.map((r, idx) => ( + { + onSelect(idx); + }} + > + + + + + + + + ))} + + + ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index d736bf2..a83cb93 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -10,7 +10,6 @@ import Toolbar from '@mui/material/Toolbar'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import * as React from 'react'; -import { Link, Outlet, useNavigate } from 'react-router-dom'; import ImportFromUrlDialog from './Layout/ImportFromUrlDialog'; @@ -18,12 +17,12 @@ const toolbarHeight = 64; // TODO Will this work everywhere? type Props = { title: string; + children: React.ReactNode; }; -export default function Layout({ title }: Props) { +export default function Layout({ title, children }: Props) { const [anchorEl, setAnchorEl] = React.useState(null); const [showImportDialog, setShowImportDialog] = React.useState(false); - const navigate = useNavigate(); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { @@ -32,9 +31,6 @@ export default function Layout({ title }: Props) { const handleClose = (e) => { setAnchorEl(null); switch (e.target.id) { - case 'new': - navigate('/editor'); - break; case 'import_from_url': setShowImportDialog(true); break; @@ -67,9 +63,6 @@ export default function Layout({ title }: Props) { 'aria-labelledby': 'basic-button', }} > - - New - Import from URL @@ -84,13 +77,13 @@ export default function Layout({ title }: Props) { {title} @@ -127,7 +120,7 @@ export default function Layout({ title }: Props) { overflow: 'auto', }} > - + {children} diff --git a/src/components/Layout/ImportFromUrlDialog.tsx b/src/components/Layout/ImportFromUrlDialog.tsx index ef756df..4f2a0a8 100644 --- a/src/components/Layout/ImportFromUrlDialog.tsx +++ b/src/components/Layout/ImportFromUrlDialog.tsx @@ -1,20 +1,10 @@ -import FileOpenIcon from '@mui/icons-material/FileOpen'; -import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemAvatar from '@mui/material/ListItemAvatar'; -import ListItemText from '@mui/material/ListItemText'; import TextField from '@mui/material/TextField'; import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import fetcha, { FetchaFile } from '../../lib/fetcha'; type Props = { onClose: () => void; @@ -22,9 +12,6 @@ type Props = { export default function ImportFromUrlDialog({ onClose }: Props) { const [url, setUrl] = useState(''); - const [options, setOptions] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); const handleUrlChange = (event: React.ChangeEvent) => { setUrl(event.target.value); @@ -34,92 +21,27 @@ export default function ImportFromUrlDialog({ onClose }: Props) { onClose(); }; - const downloadAndNavigate = async (file: FetchaFile) => { - const request = await fetch(file.url); - - const arrayBuffer = await request.arrayBuffer(); - - navigate('/editor', { - state: { - file: new File([arrayBuffer], file.url), - mode: 'customizer', - }, - }); - - onClose(); - }; - - const handleImport = async () => { - setIsLoading(true); - const response = await fetcha(url || ''); - - if (response.length === 1) { - downloadAndNavigate(response[0]); - } else { - setOptions(response); - } - - setIsLoading(false); + const handleImport = () => { + window.location.href = '__WEBSITE_URL?' + encodeURIComponent(url); }; - let content; - if (isLoading) { - content = ; - } else if (options.length > 0) { - content = ( - <> -

Please select one of them

- - {options.map((r) => ( - { - downloadAndNavigate(r); - onClose(); - }} - > - - - - - - - - ))} - - - ); - } else { - content = ( - - ); - } - return ( - - {options.length > 1 - ? 'Multiple scad files found at the provided URL' - : 'Import from URL'} - - {content} + Import from URL + + + - diff --git a/src/hooks/useImport.tsx b/src/hooks/useImport.tsx new file mode 100644 index 0000000..0d336dc --- /dev/null +++ b/src/hooks/useImport.tsx @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; + +import fetcha, { FetchaFile } from '../lib/fetcha'; + +export default function useImport(url?: string) { + const [files, setFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + + useEffect(() => { + if (url) { + (async () => { + setIsLoading(true); + + try { + const files = await fetcha(url); + setFiles(files); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + })(); + } + }, [url]); + + return { error, files, isLoading }; +} diff --git a/src/index.tsx b/src/index.tsx index db0a61a..2d8eaa8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,15 +7,18 @@ import { SnackbarProvider, enqueueSnackbar } from 'notistack'; import React from 'react'; import { createRoot } from 'react-dom/client'; +import App from './App'; +import Layout from './components/Layout'; import OpenscadWorkerProvider from './components/OpenscadWorkerProvider'; -import RouterProvider from './router'; createRoot(document.getElementById('root')).render( - + + + ); diff --git a/src/lib/fetcha.ts b/src/lib/fetcha.ts index add2e64..153c1cd 100644 --- a/src/lib/fetcha.ts +++ b/src/lib/fetcha.ts @@ -3,6 +3,7 @@ import printablesComFetcha from './fetcha/printables.com'; export type FetchaFile = { name: string; url: string; + description?: string; }; export default async function fetcha(url: string): Promise { @@ -14,7 +15,7 @@ export default async function fetcha(url: string): Promise { case 'printables.com': case 'www.printables.com': return await printablesComFetcha(url, '.scad'); - default: + default: { const urlParts = url.split('/'); let fileName = 'unknown'; @@ -28,5 +29,6 @@ export default async function fetcha(url: string): Promise { url: 'https://corsproxy.io/?' + encodeURIComponent(url), }, ]; + } } } diff --git a/src/lib/fetcha/printables.com.ts b/src/lib/fetcha/printables.com.ts index d47b0de..7445076 100644 --- a/src/lib/fetcha/printables.com.ts +++ b/src/lib/fetcha/printables.com.ts @@ -1,5 +1,12 @@ import { FetchaFile } from '../fetcha'; +type Stl = { + id: number; + name: string; + fileSize: number; + // filePreviewPath: string; // Always empty in my tests +}; + export default async function printablesComFetcha( url: string, fileNameFilter?: string @@ -48,7 +55,7 @@ export default async function printablesComFetcha( // Iterate the stls and start getting the download links for them const downloadLinkPromises = printProfile.data.print.stls .filter((stl) => stl.name.match(fileNameFilter || '')) // Filter out files that don't match the filter - .map((stl) => addDownloadLink(stl.id, id, stl.name)); + .map((stl) => addDownloadLink(stl, id)); // Wait for all the download links to be fetched const downloadLinks = await Promise.all(downloadLinkPromises); @@ -56,11 +63,7 @@ export default async function printablesComFetcha( return downloadLinks; } -async function addDownloadLink( - id: number, - printId: number, - name: string -): Promise { +async function addDownloadLink(stl: Stl, printId: number): Promise { const response = await fetch( `https://corsproxy.io/?${encodeURIComponent( 'https://api.printables.com/graphql/' @@ -75,7 +78,7 @@ async function addDownloadLink( body: JSON.stringify({ operationName: 'GetDownloadLink', variables: { - id, + id: stl.id, fileType: 'stl', // It doesn't matter if we use `stl` and download a .scad file printId, source: 'model_detail', @@ -91,7 +94,8 @@ async function addDownloadLink( const json = await response.json(); return { - name, + description: `Printables.com - ${stl.fileSize} bytes`, + name: stl.name, url: json.data.getDownloadLink.output.link, }; } diff --git a/src/pages/Import.tsx b/src/pages/Import.tsx deleted file mode 100644 index 00b7372..0000000 --- a/src/pages/Import.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useEffect } from 'react'; -import { useLoaderData, useNavigate } from 'react-router-dom'; - -import fetcha, { FetchaFile } from '../lib/fetcha'; - -type Props = { - error?: string; -}; - -type LoaderData = { - ok: boolean; - error?: string; - files: FetchaFile[]; -}; - -export default function Import({ error }: Props) { - const result = useLoaderData() as LoaderData; - const [file, setFile] = React.useState( - result.files?.length && result.files?.length > 1 ? null : result.files[0] - ); - const navigate = useNavigate(); - - useEffect(() => { - (async () => { - if (file !== null) { - // Download the file - const request = await fetch(file!.url); - - const arrayBuffer = await request.arrayBuffer(); - - navigate('/editor', { - state: { - file: new File([arrayBuffer], file.url), - mode: 'customizer', - }, - }); - } - })(); - }, [file, result]); - - if (error) { - return
{error}
; - } - if (result.error) { - return
{result.error}
; - } - - // If we have no results, we need to ask the user to provide a valid URL - if (!result || result.files?.length === 0) { - return
No scad files found at the provided URL
; - } - - // If we have more then one result, we need to ask the user which one they want to use - return ( -
-

Multiple scad files found at the provided URL

-

Please select one of them

-
    - {result.files?.map((r) => ( -
  • { - e.preventDefault(); - setFile(r); - }} - > - {r.url} -
  • - ))} -
-
- ); -} - -export async function loader({ request }) { - const url = new URL(request.url); - const urlParam = url.searchParams.get('url'); - - if (!urlParam) { - return { - error: 'No url parameter provided to import', - files: [], - ok: false, - }; - } - - // Check if the urlParam is a valid URL - try { - new URL(urlParam || ''); - } catch (e) { - return { - error: "Url doesn't seem to be valid: " + e.message, - files: [], - ok: false, - }; - } - - const result = await fetcha(urlParam || ''); - - return { - ok: true, - files: result, - }; -} diff --git a/src/router.tsx b/src/router.tsx deleted file mode 100644 index 246c3ba..0000000 --- a/src/router.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { - RouterProvider as ReactRouterProvider, - createHashRouter, -} from 'react-router-dom'; - -import Layout from './components/Layout'; -import Editor from './pages/Editor'; -import Import, { loader } from './pages/Import'; - -export default function RouterProvider() { - const router = createHashRouter([ - { - path: '/', - element: , - children: [ - { - path: '/', - element: , - }, - { - path: '/import', - element: , - loader: loader, - }, - { - path: '/editor', - element: , - }, - ], - }, - ]); - - return ; -}