├── .babelrc.json ├── .gitignore ├── .prettierrc ├── .storybook ├── index.css ├── main.ts └── preview.ts ├── .yarnrc.yml ├── LICENSE ├── README.md ├── demo ├── .gitignore ├── README.md ├── index.html ├── modal-test-js.html ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── yarn.lock ├── package-js.json ├── package.json ├── rollup.config-js.js ├── rollup.config.js ├── src ├── components │ └── CSVImporter │ │ ├── CSVImporter.stories.tsx │ │ ├── index.tsx │ │ └── style │ │ └── csv-importer.css ├── i18n │ ├── de.ts │ ├── es.ts │ ├── fr.ts │ ├── i18n.ts │ └── it.ts ├── importer │ ├── components │ │ ├── Box │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Box.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Checkbox │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Checkbox.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Errors │ │ │ ├── index.tsx │ │ │ └── style │ │ │ │ └── Errors.module.scss │ │ ├── Input │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ ├── Input.module.scss │ │ │ │ └── mixins.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Portal │ │ │ ├── index.tsx │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Stepper │ │ │ ├── hooks │ │ │ │ └── useStepper.ts │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Stepper.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Table │ │ │ ├── index.tsx │ │ │ ├── storyData.ts │ │ │ ├── style │ │ │ │ └── Default.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── ToggleFilter │ │ │ ├── ToggleFilter.stories.tsx │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── ToggleFilter.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── Tooltip │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Tooltip.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ └── UploaderWrapper │ │ │ ├── UploaderWrapper.tsx │ │ │ ├── style │ │ │ └── uppy.overrides.scss │ │ │ └── types │ │ │ └── index.ts │ ├── features │ │ ├── complete │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Complete.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── main │ │ │ ├── hooks │ │ │ │ ├── useMutableLocalStorage.ts │ │ │ │ └── useStepNavigation.ts │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── Main.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── map-columns │ │ │ ├── components │ │ │ │ └── DropDownFields.tsx │ │ │ ├── hooks │ │ │ │ ├── useMapColumnsTable.tsx │ │ │ │ └── useNameChange.ts │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── MapColumns.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ ├── row-selection │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ │ └── RowSelection.module.scss │ │ │ └── types │ │ │ │ └── index.ts │ │ └── uploader │ │ │ ├── hooks │ │ │ └── useTemplateTable.tsx │ │ │ ├── index.tsx │ │ │ ├── style │ │ │ └── Uploader.module.scss │ │ │ └── types │ │ │ └── index.ts │ ├── hooks │ │ ├── useClickOutside.ts │ │ ├── useCustomStyles.ts │ │ ├── useDelayLoader.ts │ │ ├── useEventListener.ts │ │ ├── useIsomorphicLayoutEffect.ts │ │ ├── useRect.ts │ │ └── useWindowSize.ts │ ├── providers │ │ ├── Theme.tsx │ │ ├── index.tsx │ │ └── types │ │ │ └── index.ts │ ├── settings │ │ ├── chakra │ │ │ ├── components │ │ │ │ ├── alert.ts │ │ │ │ ├── button.ts │ │ │ │ └── index.ts │ │ │ ├── foundations │ │ │ │ ├── blur.ts │ │ │ │ ├── borders.ts │ │ │ │ ├── breakpoints.ts │ │ │ │ ├── colors.ts │ │ │ │ ├── index.ts │ │ │ │ ├── radius.ts │ │ │ │ ├── shadows.ts │ │ │ │ ├── sizes.ts │ │ │ │ ├── spacing.ts │ │ │ │ ├── transition.ts │ │ │ │ ├── typography.ts │ │ │ │ └── z-index.ts │ │ │ ├── index.ts │ │ │ ├── semantic-tokens.ts │ │ │ ├── styles.ts │ │ │ ├── theme.types.ts │ │ │ └── utils │ │ │ │ ├── is-chakra-theme.ts │ │ │ │ └── run-if-fn.ts │ │ └── theme │ │ │ ├── colors.ts │ │ │ ├── index.ts │ │ │ └── sizes.ts │ ├── stores │ │ └── theme.ts │ ├── style │ │ ├── design-system │ │ │ └── colors.scss │ │ ├── fonts.scss │ │ ├── index.scss │ │ ├── mixins.scss │ │ ├── themes │ │ │ ├── common.scss │ │ │ ├── dark.scss │ │ │ └── light.scss │ │ └── vars.scss │ ├── types │ │ └── index.ts │ └── utils │ │ ├── classes.ts │ │ ├── debounce.ts │ │ ├── getStringLengthOfChildren.ts │ │ ├── stringSimilarity.ts │ │ ├── template.ts │ │ └── utils.ts ├── index.ts ├── js.tsx ├── settings │ └── defaults.ts ├── styles.d.ts └── types │ └── index.ts ├── tsconfig.json └── yarn.lock /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.iml 3 | *.swp 4 | *.swo 5 | *.dll 6 | *.so 7 | *.dylib 8 | *.test 9 | *.out 10 | *.log 11 | *.env 12 | *.zip 13 | 14 | # debug 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | node_modules/ 20 | vendor/ 21 | build/ 22 | .yarn/ 23 | .pnp* 24 | 25 | .DS_Store 26 | .idea 27 | .vscode 28 | 29 | /coverage 30 | /.next/ 31 | /out/ 32 | 33 | # misc 34 | *.pem 35 | 36 | # local env files 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | next-env.d.ts 45 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "tabWidth": 2, 5 | "jsxSingleQuote": false, 6 | "bracketSameLine": true, 7 | "printWidth": 150, 8 | "endOfLine": "auto", 9 | "importOrder": [ 10 | "^[^/.]+$", 11 | "^@.+$", 12 | "^\\.\\./(.*)/[A-Z]((?!\\.)(?!types.).)+$", 13 | "^\\.\\./(.*)/((?!\\.)(?!types.).)+$", 14 | "^\\.$", 15 | "^\\./(.*)/[A-Z]((?!\\.)(?!types.).)+$", 16 | "^\\./(.*)/((?!\\.)(?!types.).)+$", 17 | "/types$", 18 | "\\.(css|scss)$", 19 | "\\.(svg|png|jpg|jpeg|gif)$", 20 | ".*" 21 | ], 22 | "importOrderSeparation": false, 23 | "importOrderSortSpecifiers": true, 24 | "importOrderCaseInsensitive": true 25 | } 26 | -------------------------------------------------------------------------------- /.storybook/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #efefef; 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import type { StorybookConfig } from "@storybook/react-webpack5"; 3 | 4 | const config: StorybookConfig = { 5 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 6 | addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", "@storybook/preset-create-react-app"], 7 | framework: { 8 | name: "@storybook/react-webpack5", 9 | options: {}, 10 | }, 11 | docs: { 12 | autodocs: "tag", 13 | }, 14 | core: { 15 | builder: "@storybook/builder-webpack5", 16 | }, 17 | webpackFinal: (config) => { 18 | config.module = config.module || { rules: [] }; 19 | config.module.rules = config.module.rules || []; 20 | 21 | config.module.rules.push({ 22 | test: /\.s(a|c)ss$/, 23 | include: path.resolve(__dirname, "../"), 24 | use: [ 25 | "style-loader", 26 | { 27 | loader: "css-loader", 28 | options: { 29 | modules: { 30 | auto: true, 31 | localIdentName: "[name]__[local]--[hash:base64:5]", 32 | }, 33 | }, 34 | }, 35 | "sass-loader", 36 | ], 37 | }); 38 | config.module.rules.push({ 39 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 40 | type: "asset/resource", 41 | }); 42 | return config; 43 | }, 44 | }; 45 | export default config; 46 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | 4 | export const parameters = { 5 | actions: { argTypesRegex: "^on[A-Z].*" }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Portola Labs, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | CSV Import 4 | 5 | Open-source CSV and XLS/XLSX file importer for React and JavaScript 6 | 7 |
8 | 9 | ## How It Works 10 | 11 | 1. Embed the CSV Importer in your app with the [React](https://www.npmjs.com/package/csv-import-react) 12 | or [JavaScript](https://www.npmjs.com/package/csv-import-js) SDK 13 | 2. Define the columns your users can import (via the `template` parameter) 14 | 3. Your users import their files in your app 15 | 4. Retrieve the imported data from the `onComplete` event 16 | 17 | ![Importer Modal](https://tableflow-assets-cdn.s3.amazonaws.com/importer-modal-20230613b.png) 18 | 19 | ## Get Started 20 | 21 | ### 1. Install SDK 22 | 23 | Use NPM or Yarn to install the SDK for [React](https://www.npmjs.com/package/csv-import-react) 24 | or [JavaScript](https://www.npmjs.com/package/csv-import-js). 25 | 26 | **NPM** 27 | 28 | ```bash 29 | npm install csv-import-react 30 | # or 31 | npm install csv-import-js 32 | ``` 33 | 34 | **Yarn** 35 | 36 | ```bash 37 | yarn add csv-import-react 38 | # or 39 | yarn add csv-import-js 40 | ``` 41 | 42 | ### 2. Add the importer to your application 43 | 44 | #### Using React 45 | 46 | ```javascript 47 | import { CSVImporter } from "csv-import-react"; 48 | import { useState } from "react"; 49 | 50 | function MyComponent() { 51 | const [isOpen, setIsOpen] = useState(false); 52 | 53 | return ( 54 | <> 55 | 56 | 57 | setIsOpen(false)} 60 | darkMode={true} 61 | onComplete={(data) => console.log(data)} 62 | template={{ 63 | columns: [ 64 | { 65 | name: "First Name", 66 | key: "first_name", 67 | required: true, 68 | description: "The first name of the user", 69 | suggested_mappings: ["First", "Name"], 70 | }, 71 | { 72 | name: "Age", 73 | data_type: "number", 74 | }, 75 | ], 76 | }} 77 | /> 78 | 79 | ); 80 | } 81 | ``` 82 | 83 | #### Using JavaScript 84 | 85 | ```html 86 | 87 | 88 | 89 | 90 | 91 |
92 | 120 | 121 | ``` 122 | 123 | ## SDK Reference 124 | 125 | ### isModal (_boolean_, default: `true`) 126 | 127 | When set to `true` (default value), the importer will behave as a modal with its open state controlled by `modalIsOpen`. When set to `false`, the importer will be embedded directly in your page. 128 | 129 | ### modalIsOpen (_boolean_, default: `false`) 130 | 131 | Only used when `isModal` is `true`: Controls the importer modal being open or closed. 132 | \ 133 | **React SDK Only**: For the JavaScript SDK, use `.showModal()` and `.closeModal()` to operate the modal. 134 | 135 | ### modalOnCloseTriggered (_function_) 136 | 137 | Only used when `isModal` is `true`: A function called when the user clicks the close button or clicks outside of (when used with `modalCloseOnOutsideClick`) the importer. `useState` can be used to control the importer modal opening and closing. 138 | 139 | ```javascript 140 | const [isOpen, setIsOpen] = useState(false); 141 | ``` 142 | 143 | ```jsx 144 | 145 | setIsOpen(false)} 148 | ... 149 | /> 150 | ``` 151 | 152 | ### modalCloseOnOutsideClick (_boolean_, default: `false`) 153 | 154 | Only used when `isModal` is `true`: Clicking outside the modal will call the `modalOnCloseTriggered` function. 155 | 156 | ### template (_object_) 157 | 158 | Configure the columns used for the import. 159 | 160 | ```jsx 161 | template={{ 162 | columns: [ 163 | { 164 | name: "First Name", 165 | key: "first_name", 166 | required: true, 167 | description: "The first name of the user", 168 | suggested_mappings: ["First", "Name"], 169 | }, 170 | { 171 | name: "Age", 172 | data_type: "number", 173 | }, 174 | ], 175 | }} 176 | ``` 177 | 178 | ### onComplete (_function_) 179 | 180 | Callback function that fires when a user completes an import. It returns `data`, an object that contains the row data, column definitions, and other information about the import. 181 | 182 | ```jsx 183 | onComplete={(data) => console.log(data)} 184 | ``` 185 | 186 | Example `data`: 187 | 188 | ```json 189 | { 190 | "num_rows": 2, 191 | "num_columns": 3, 192 | "columns": [ 193 | { 194 | "key": "age", 195 | "name": "Age" 196 | }, 197 | { 198 | "key": "email", 199 | "name": "Email" 200 | }, 201 | { 202 | "key": "first_name", 203 | "name": "First Name" 204 | } 205 | ], 206 | "rows": [ 207 | { 208 | "index": 0, 209 | "values": { 210 | "age": 23, 211 | "email": "maria@example.com", 212 | "first_name": "Maria" 213 | } 214 | }, 215 | { 216 | "index": 1, 217 | "values": { 218 | "age": 32, 219 | "email": "robert@example.com", 220 | "first_name": "Robert" 221 | } 222 | } 223 | ] 224 | } 225 | ``` 226 | 227 | ### darkMode (_boolean_, default: `false`) 228 | 229 | Toggle between dark mode (`true`) and light mode (`false`). 230 | 231 | ### primaryColor (_string_) 232 | 233 | Specifies the primary color for the importer in hex format. Use `customStyles` to customize the UI in more detail. 234 | 235 | ```jsx 236 | primaryColor="#7A5EF8" 237 | ``` 238 | 239 | ### customStyles (_object_) 240 | 241 | Apply custom styles to the importer with an object containing CSS properties and values. Note that custom style properties will override `primaryColor` and any default styles from `darkMode`. 242 | Available options: 243 | 244 | ```jsx 245 | customStyles={{ 246 | "font-family": "cursive", 247 | "font-size": "15px", 248 | "base-spacing": "2rem", 249 | "border-radius": "8px", 250 | "color-primary": "salmon", 251 | "color-primary-hover": "crimson", 252 | "color-secondary": "indianRed", 253 | "color-secondary-hover": "crimson", 254 | "color-tertiary": "indianRed", 255 | "color-tertiary-hover": "crimson", 256 | "color-border": "lightCoral", 257 | "color-text": "brown", 258 | "color-text-soft": "rgba(165, 42, 42, .5)", 259 | "color-text-on-primary": "#fff", 260 | "color-text-on-secondary": "#ffffff", 261 | "color-background": "bisque", 262 | "color-background-modal": "blanchedAlmond", 263 | "color-input-background": "blanchedAlmond", 264 | "color-input-background-soft": "white", 265 | "color-background-menu-hover": "bisque", 266 | "color-importer-link": "indigo", 267 | "color-progress-bar": "darkGreen" 268 | }} 269 | ``` 270 | 271 | ## Internationalization 272 | 273 | ### Predefined languages 274 | - Out-of-the-box support for various languages. 275 | - Common languages are available through the language prop (i.e., `language="fr"` for French). 276 | - Available predefined languages: 277 | - en 278 | - es 279 | - fr 280 | 281 | ### Customizable language 282 | - Language keys can be exported and overridden. 283 | - Labels and messages can be customized to any text. 284 | - Translations key examples can be found in `src/i18n/es.ts` 285 | 286 | ```javascript 287 | // Set up custom translations 288 | const customTranslations = { 289 | jp: { 290 | Upload: "アップロード", 291 | "Browse files": "ファイルを参照", 292 | }, 293 | pt: { 294 | Upload: "Carregar", 295 | "Browse files": "Procurar arquivos", 296 | }, 297 | }; 298 | 299 | return ( 300 | 301 | ) 302 | 303 | ``` 304 | 305 | ### showDownloadTemplateButton (_boolean_, default: `true`) 306 | 307 | When set to `false`, hide the Download Template button on the first screen of the importer. 308 | 309 | ### skipHeaderRowSelection (_boolean_, default: `false`) 310 | 311 | When set to `true`, the importer will not display and skip the Header Row Selection step and always choose the first row in the file as the header. 312 | 313 | ## Contributing 314 | 315 | ### Setting Up the Project 316 | 317 | To set up the project locally, follow these steps: 318 | 319 | 1. **Clone the repository** 320 | ```bash 321 | git clone https://github.com/tableflowhq/csv-import.git 322 | cd csv-import 323 | ``` 324 | 325 | 2. **Install dependencies** 326 | ```bash 327 | yarn install 328 | ``` 329 | 330 | 3. **Build the project** 331 | ```bash 332 | yarn build 333 | ``` 334 | 335 | ### Running Storybook 336 | To run Storybook locally, follow these steps: 337 | 338 | 1. **Start Storybook** 339 | ```bash 340 | yarn storybook 341 | ``` 342 | 343 | 2. **Open Storybook in your browser:** 344 | Storybook should automatically open in your default browser. If it doesn't, navigate to [http://localhost:6006](http://localhost:6006). 345 | 346 | ### Modifying the project and testing with the demo app 347 | 348 | The project includes a demo app that you can use to test your changes. The demo app has its own `README.md` file with detailed instructions on how to set it up and run it. 349 | 350 | 1. Make your changes in the codebase. 351 | 2. Follow the instructions in the demo app's `README.md` to set up and run the demo app. This will help you verify that your changes work as expected in a real application. 352 | 3. Commit your changes and push them to your forked repository. 353 | 4. Create a pull request to the main repository. 354 | 355 | ## Get In Touch 356 | 357 | Let us know your feedback or feature requests! Submit a GitHub 358 | issue [here](https://github.com/tableflowhq/csv-import/issues/new). 359 | -------------------------------------------------------------------------------- /demo/.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 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .yalc 25 | yalc.lock 26 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## How to use the Demo project 2 | 3 | 1. Make sure you have yalc installed, `yarn global add yalc` 4 | 2. In the project directory, you can build and then publish this package locally using: 5 | ```bash 6 | yarn build && yalc publish 7 | ``` 8 | 3. cd to the demo/ folder and add the published package 9 | ```bash 10 | yalc add csv-import 11 | ``` 12 | 13 | ### `yarn start` 14 | 15 | Runs the app in the development mode.\ 16 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 17 | 18 | The page will reload when you make changes.\ 19 | You may also see any lint errors in the console. 20 | 21 | ### `npm run build` 22 | 23 | Builds the app for production to the `build` folder.\ 24 | It correctly bundles React in production mode and optimizes the build for the best performance. 25 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /demo/modal-test-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-sdk", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "assert": "^2.1.0", 10 | "constants": "^0.0.2", 11 | "crypto": "^1.0.1", 12 | "csv-import-react": "^1.0.6", 13 | "fs": "^0.0.1-security", 14 | "http": "^0.0.1-security", 15 | "https": "^1.0.0", 16 | "os": "^0.1.2", 17 | "path": "^0.12.7", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-scripts": "5.0.1", 21 | "stream": "^0.0.2", 22 | "tty": "^1.0.1", 23 | "url": "^0.11.3", 24 | "util": "^0.12.5", 25 | "web-vitals": "^2.1.4", 26 | "zlib": "^1.0.5" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /demo/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/logo192.png -------------------------------------------------------------------------------- /demo/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflowhq/csv-import/0b03fe7ce87ba02822acf1851119267af5a0a789/demo/public/logo512.png -------------------------------------------------------------------------------- /demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /demo/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background-color: var(--color-primary); 3 | border: none; 4 | color: white; 5 | padding: 15px 32px; 6 | text-align: center; 7 | text-decoration: none; 8 | display: inline-block; 9 | font-size: 16px; 10 | } 11 | 12 | .App { 13 | height: 100vh; 14 | width: 100vw; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | text-align: center; 19 | } 20 | 21 | .App-logo { 22 | height: 40vmin; 23 | pointer-events: none; 24 | } 25 | 26 | @media (prefers-reduced-motion: no-preference) { 27 | .App-logo { 28 | animation: App-logo-spin infinite 20s linear; 29 | } 30 | } 31 | 32 | .App-header { 33 | background-color: #282c34; 34 | min-height: 100vh; 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | justify-content: center; 39 | font-size: calc(10px + 2vmin); 40 | color: white; 41 | } 42 | 43 | .App-link { 44 | color: #61dafb; 45 | } 46 | 47 | @keyframes App-logo-spin { 48 | from { 49 | transform: rotate(0deg); 50 | } 51 | to { 52 | transform: rotate(360deg); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/src/App.js: -------------------------------------------------------------------------------- 1 | import { CSVImporter } from "csv-import-react"; 2 | import { useState } from "react"; 3 | import "./App.css"; 4 | 5 | function App() { 6 | const template = { 7 | columns: [ 8 | { 9 | name: "First Name", 10 | key: "first_name", 11 | required: true, 12 | description: "The first name of the user", 13 | suggested_mappings: ["first", "mame"], 14 | }, 15 | { 16 | name: "Last Name", 17 | suggested_mappings: ["last"], 18 | }, 19 | { 20 | name: "Email", 21 | required: true, 22 | description: "The email of the user", 23 | }, 24 | ], 25 | }; 26 | const [isOpen, setIsOpen] = useState(false); 27 | return ( 28 |
29 | 32 | 33 | setIsOpen(false)} 39 | modalCloseOnOutsideClick={true} 40 | /> 41 |
42 | ); 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /demo/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /demo/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /package-js.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-import-js", 3 | "version": "1.0.14", 4 | "description": "Open-source CSV and XLS/XLSX file importer for React and JavaScript", 5 | "main": "index.js", 6 | "files": ["."], 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/tableflowhq/csv-import.git" 10 | }, 11 | "keywords": ["csv", "import", "excel", "data", "importer", "react", "tableflow", "csv-import"], 12 | "author": "TableFlow", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/tableflowhq/csv-import/issues" 16 | }, 17 | "homepage": "https://github.com/tableflowhq/csv-import#readme", 18 | "types": "index.d.ts", 19 | "peerDependencies": { 20 | "react": ">=18.0.0", 21 | "react-dom": ">=18.0.0" 22 | }, 23 | "devDependencies": { 24 | "@babel/preset-env": "^7.22.4", 25 | "@babel/preset-react": "^7.22.3", 26 | "@babel/preset-typescript": "^7.21.5", 27 | "@rollup/plugin-commonjs": "^17.1.0", 28 | "@rollup/plugin-image": "^2.1.1", 29 | "@rollup/plugin-node-resolve": "^11.2.1", 30 | "@rollup/plugin-replace": "^5.0.5", 31 | "@storybook/addon-essentials": "7.0.18", 32 | "@storybook/addon-interactions": "7.0.18", 33 | "@storybook/addon-links": "7.0.18", 34 | "@storybook/blocks": "7.0.18", 35 | "@storybook/react": "7.0.18", 36 | "@storybook/react-webpack5": "7.0.18", 37 | "@storybook/testing-library": "0.0.14-next.2", 38 | "@testing-library/jest-dom": "^5.14.1", 39 | "@testing-library/react": "^11.2.7", 40 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 41 | "@types/babel__core": "^7.20.1", 42 | "@types/jest": "^24.9.1", 43 | "@types/papaparse": "^5.3.14", 44 | "@types/react": "^18.0.0", 45 | "@types/react-dom": "^18.0.0", 46 | "@types/use-sync-external-store": "0.0.3", 47 | "@types/xlsx": "^0.0.36", 48 | "@typescript-eslint/eslint-plugin": "^5.56.0", 49 | "@typescript-eslint/parser": "^5.56.0", 50 | "core-js": "^3.22.7", 51 | "css-loader": "^6.8.1", 52 | "eslint-config-prettier": "^8.8.0", 53 | "eslint-plugin-prettier": "^4.2.1", 54 | "eslint-plugin-react": "^7.32.2", 55 | "identity-obj-proxy": "^3.0.0", 56 | "jest": "^26.6.3", 57 | "node-sass": "^9.0.0", 58 | "prettier": "^2.8.6", 59 | "prop-types": "^15.8.1", 60 | "react": "18.2.0", 61 | "react-dom": "18.2.0", 62 | "rollup": "^2.56.3", 63 | "rollup-plugin-copy": "^3.4.0", 64 | "rollup-plugin-peer-deps-external": "^2.2.4", 65 | "rollup-plugin-postcss": "^3.1.8", 66 | "rollup-plugin-typescript2": "^0.29.0", 67 | "sass": "^1.69.7", 68 | "sass-loader": "^13.3.3", 69 | "storybook": "7.0.18", 70 | "style-loader": "^3.3.3", 71 | "ts-jest": "^26.5.6", 72 | "typescript": "^4.5.5", 73 | "typescript-plugin-css-modules": "^3.4.0", 74 | "use-sync-external-store": "^1.2.0" 75 | }, 76 | "dependencies": { 77 | "@chakra-ui/alert": "^2.2.2", 78 | "@chakra-ui/button": "^2.1.0", 79 | "@chakra-ui/react": "^2.8.1", 80 | "@chakra-ui/system": "^2.6.2", 81 | "@emotion/react": "^11.11.3", 82 | "@emotion/styled": "^11.11.0", 83 | "@rollup/plugin-json": "^6.1.0", 84 | "framer-motion": "^10.17.12", 85 | "papaparse": "^5.4.1", 86 | "react-dropzone": "^14.2.3", 87 | "react-icons": "^4.12.0", 88 | "xlsx": "^0.18.5", 89 | "zustand": "^4.4.7" 90 | }, 91 | "type": "module" 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csv-import-react", 3 | "version": "1.0.14", 4 | "description": "Open-source CSV and XLS/XLSX file importer for React and JavaScript", 5 | "main": "build/index.js", 6 | "module": "build/index.esm.js", 7 | "files": [ 8 | "build" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/tableflowhq/csv-import.git" 13 | }, 14 | "keywords": [ 15 | "csv", 16 | "import", 17 | "excel", 18 | "data", 19 | "importer", 20 | "react", 21 | "tableflow", 22 | "csv-import" 23 | ], 24 | "author": "TableFlow", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/tableflowhq/csv-import/issues" 28 | }, 29 | "homepage": "https://github.com/tableflowhq/csv-import#readme", 30 | "types": "build/index.d.ts", 31 | "scripts": { 32 | "build:js": "rollup --config rollup.config-js.js && cp package-js.json build/package.json", 33 | "build:react": "rollup --config rollup.config.js", 34 | "publish:local:js": "yarn build:js && cd build && yalc publish", 35 | "publish:local:react": "yarn build:react && yalc publish", 36 | "publish:js": "yarn build:js && cd build && npm publish", 37 | "publish:react": "yarn build:react && npm publish", 38 | "build:watch": "rollup -c -w", 39 | "storybook": "storybook dev -p 6006", 40 | "storybook:export": "build-storybook", 41 | "generate": "node ./util/create-component", 42 | "build-storybook": "storybook build" 43 | }, 44 | "peerDependencies": { 45 | "react": ">=18.0.0", 46 | "react-dom": ">=18.0.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/preset-env": "^7.22.4", 50 | "@babel/preset-react": "^7.22.3", 51 | "@babel/preset-typescript": "^7.21.5", 52 | "@rollup/plugin-commonjs": "^17.1.0", 53 | "@rollup/plugin-image": "^2.1.1", 54 | "@rollup/plugin-node-resolve": "^11.2.1", 55 | "@rollup/plugin-replace": "^5.0.5", 56 | "@storybook/addon-essentials": "7.0.18", 57 | "@storybook/addon-interactions": "7.0.18", 58 | "@storybook/addon-links": "7.0.18", 59 | "@storybook/blocks": "7.0.18", 60 | "@storybook/react": "7.0.18", 61 | "@storybook/react-webpack5": "7.0.18", 62 | "@storybook/testing-library": "0.0.14-next.2", 63 | "@trivago/prettier-plugin-sort-imports": "^4.1.1", 64 | "@types/babel__core": "^7.20.1", 65 | "@types/jest": "^24.9.1", 66 | "@types/papaparse": "^5.3.14", 67 | "@types/react": "^18.0.0", 68 | "@types/react-dom": "^18.0.0", 69 | "@types/use-sync-external-store": "0.0.3", 70 | "@types/xlsx": "^0.0.36", 71 | "@typescript-eslint/eslint-plugin": "^5.56.0", 72 | "@typescript-eslint/parser": "^5.56.0", 73 | "core-js": "^3.22.7", 74 | "css-loader": "^6.8.1", 75 | "eslint-config-prettier": "^8.8.0", 76 | "eslint-plugin-prettier": "^4.2.1", 77 | "eslint-plugin-react": "^7.32.2", 78 | "identity-obj-proxy": "^3.0.0", 79 | "jest": "^26.6.3", 80 | "node-sass": "^9.0.0", 81 | "prettier": "^2.8.6", 82 | "prop-types": "^15.8.1", 83 | "react": "18.2.0", 84 | "react-dom": "18.2.0", 85 | "rollup": "^2.56.3", 86 | "rollup-plugin-copy": "^3.4.0", 87 | "rollup-plugin-peer-deps-external": "^2.2.4", 88 | "rollup-plugin-postcss": "^3.1.8", 89 | "rollup-plugin-typescript2": "^0.29.0", 90 | "sass": "^1.69.7", 91 | "sass-loader": "^13.3.3", 92 | "storybook": "7.0.18", 93 | "style-loader": "^3.3.3", 94 | "ts-jest": "^26.5.6", 95 | "typescript": "^4.5.5", 96 | "typescript-plugin-css-modules": "^3.4.0", 97 | "use-sync-external-store": "^1.2.0" 98 | }, 99 | "dependencies": { 100 | "@chakra-ui/alert": "^2.2.2", 101 | "@chakra-ui/button": "^2.1.0", 102 | "@chakra-ui/react": "^2.8.1", 103 | "@chakra-ui/system": "^2.6.2", 104 | "@emotion/cache": "^11.11.0", 105 | "@emotion/react": "^11.11.3", 106 | "@emotion/styled": "^11.11.0", 107 | "@rollup/plugin-json": "^6.1.0", 108 | "framer-motion": "^10.17.12", 109 | "i18next": "^23.10.1", 110 | "papaparse": "^5.4.1", 111 | "react-dropzone": "^14.2.3", 112 | "react-i18next": "^14.1.0", 113 | "react-icons": "^4.12.0", 114 | "xlsx": "^0.18.5", 115 | "zustand": "^4.4.7" 116 | }, 117 | "type": "module" 118 | } 119 | -------------------------------------------------------------------------------- /rollup.config-js.js: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 2 | 3 | import postcss from "rollup-plugin-postcss"; 4 | import typescript from "rollup-plugin-typescript2"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | import image from "@rollup/plugin-image"; 7 | import json from "@rollup/plugin-json"; 8 | import resolve, { nodeResolve } from "@rollup/plugin-node-resolve"; 9 | import replace from "@rollup/plugin-replace"; 10 | 11 | const packageJson = require("./package.json"); 12 | 13 | export default { 14 | input: "src/js.tsx", 15 | output: [ 16 | { 17 | file: "build/index.js", 18 | format: "umd", 19 | name: "CSVImporter", 20 | sourcemap: true, 21 | }, 22 | ], 23 | plugins: [ 24 | replace({ 25 | "process.env.NODE_ENV": JSON.stringify("production"), 26 | preventAssignment: true, 27 | }), 28 | resolve({ 29 | browser: true, 30 | }), 31 | commonjs(), 32 | typescript({ useTsconfigDeclarationDir: true }), 33 | image(), 34 | postcss({}), 35 | json(), 36 | ], 37 | }; -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 2 | import postcss from "rollup-plugin-postcss"; 3 | import typescript from "rollup-plugin-typescript2"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import image from "@rollup/plugin-image"; 6 | import json from "@rollup/plugin-json"; 7 | import resolve from "@rollup/plugin-node-resolve"; 8 | 9 | const packageJson = require("./package.json"); 10 | 11 | export default { 12 | input: "src/index.ts", 13 | output: [ 14 | { 15 | file: packageJson.main, 16 | format: "cjs", 17 | sourcemap: true, 18 | }, 19 | { 20 | file: packageJson.module, 21 | format: "esm", 22 | sourcemap: true, 23 | }, 24 | ], 25 | external: ["react", "react-dom", "react/jsx-runtime", "@emotion/react"], 26 | plugins: [ 27 | peerDepsExternal(), 28 | resolve({ 29 | browser: true, 30 | }), 31 | commonjs(), 32 | typescript({ useTsconfigDeclarationDir: true }), 33 | image(), 34 | postcss({}), 35 | json(), 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/CSVImporter/CSVImporter.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { ComponentMeta, ComponentStory, Story } from "@storybook/react"; 3 | import defaults from "../../settings/defaults"; 4 | import { CSVImporterProps } from "../../types"; 5 | import ImporterComponent from "./index"; 6 | 7 | export default { 8 | title: "User Interface/Importer", 9 | component: ImporterComponent, 10 | argTypes: { 11 | primaryColor: { 12 | control: { type: "color" }, 13 | }, 14 | labelColor: { 15 | control: { type: "color" }, 16 | }, 17 | }, 18 | } as ComponentMeta; 19 | 20 | const template = { 21 | columns: [ 22 | { 23 | name: "First Name", 24 | key: "first_name", 25 | required: true, 26 | description: "The first name of the user", 27 | suggested_mappings: ["first", "mame"], 28 | }, 29 | { 30 | name: "Last Name", 31 | suggested_mappings: ["last"], 32 | }, 33 | { 34 | name: "Email", 35 | required: true, 36 | description: "The email of the user", 37 | }, 38 | ], 39 | }; 40 | 41 | const Template: Story = (args: CSVImporterProps) => { 42 | const [isOpen, setIsOpen] = useState(false); 43 | 44 | const { isModal } = args; 45 | 46 | const props = { 47 | ...(isModal ? { modalIsOpen: isOpen } : {}), 48 | ...(isModal ? { modalOnCloseTriggered: () => setIsOpen(false) } : {}), 49 | ...(isModal ? { modalCloseOnOutsideClick: args.modalCloseOnOutsideClick } : {}), 50 | ...args, 51 | }; 52 | 53 | return ( 54 |
55 | {args.isModal && } 56 | 57 |
58 | ); 59 | }; 60 | 61 | export const Importer = Template.bind({}); 62 | Importer.args = { 63 | language: "en", 64 | ...defaults, 65 | template: template, 66 | customTranslations: { 67 | jp: { 68 | Upload: "アップロード", 69 | "Browse files": "ファイルを参照", 70 | }, 71 | pt: { 72 | Upload: "Carregar", 73 | "Browse files": "Procurar arquivos", 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/CSVImporter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useRef, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import "../../i18n/i18n"; 4 | import Importer from "../../importer/features/main"; 5 | import Providers from "../../importer/providers"; 6 | import useThemeStore from "../../importer/stores/theme"; 7 | import { darkenColor, isValidColor } from "../../importer/utils/utils"; 8 | import { CSVImporterProps } from "../../types"; 9 | import "../../importer/style/index.scss"; 10 | import "./style/csv-importer.css"; 11 | 12 | const CSVImporter = forwardRef((importerProps: CSVImporterProps, forwardRef?: any) => { 13 | const { 14 | isModal = true, 15 | modalIsOpen = true, 16 | modalOnCloseTriggered = () => null, 17 | modalCloseOnOutsideClick, 18 | template, 19 | darkMode = false, 20 | primaryColor = "#7a5ef8", 21 | className, 22 | onComplete, 23 | customStyles, 24 | showDownloadTemplateButton, 25 | skipHeaderRowSelection, 26 | language, 27 | customTranslations, 28 | ...props 29 | } = importerProps; 30 | const ref = forwardRef ?? useRef(null); 31 | 32 | const { t, i18n } = useTranslation(); 33 | const current = ref?.current as any; 34 | 35 | useEffect(() => { 36 | i18n.changeLanguage(language); 37 | }, [language]); 38 | 39 | useEffect(() => { 40 | if (customTranslations) { 41 | Object.keys(customTranslations).forEach((language) => { 42 | i18n.addResourceBundle(language, "translation", customTranslations[language], true, true); 43 | }); 44 | } 45 | }, [customTranslations]); 46 | 47 | useEffect(() => { 48 | if (isModal && current) { 49 | if (modalIsOpen) current?.showModal?.(); 50 | else current?.close?.(); 51 | } 52 | }, [isModal, modalIsOpen, current]); 53 | const baseClass = "CSVImporter"; 54 | const themeClass = darkMode ? `${baseClass}-dark` : `${baseClass}-light`; 55 | const domElementClass = ["csv-importer", `${baseClass}-${isModal ? "dialog" : "div"}`, themeClass, className].filter((i) => i).join(" "); 56 | 57 | // Set Light/Dark mode 58 | const setTheme = useThemeStore((state) => state.setTheme); 59 | 60 | useEffect(() => { 61 | const theme = darkMode ? "dark" : "light"; 62 | setTheme(theme); 63 | }, [darkMode]); 64 | 65 | // Apply primary color 66 | useEffect(() => { 67 | if (primaryColor && isValidColor(primaryColor)) { 68 | const root = document.documentElement; 69 | root.style.setProperty("--color-primary", primaryColor); 70 | root.style.setProperty("--color-primary-hover", darkenColor(primaryColor, 20)); 71 | } 72 | }, [primaryColor]); 73 | 74 | const backdropClick = (event: { target: any }) => { 75 | if (modalCloseOnOutsideClick && event.target === current) { 76 | modalOnCloseTriggered(); 77 | } 78 | }; 79 | 80 | current?.addEventListener("cancel", () => { 81 | modalOnCloseTriggered(); 82 | }); 83 | 84 | const elementProps = { 85 | ref, 86 | ...(isModal ? { onClick: backdropClick } : {}), 87 | className: domElementClass, 88 | "data-theme": darkMode ? "dark" : "light", 89 | style: { colorScheme: darkMode ? "dark" : "light" }, 90 | ...props, 91 | }; 92 | 93 | const ImporterComponent = () => ( 94 | 95 | 96 | 97 | ); 98 | 99 | return isModal ? ( 100 |
101 | 102 | 103 | 104 |
105 | ) : ( 106 |
107 | 108 |
109 | ); 110 | }); 111 | 112 | export default CSVImporter; 113 | -------------------------------------------------------------------------------- /src/components/CSVImporter/style/csv-importer.css: -------------------------------------------------------------------------------- 1 | .CSVImporter { 2 | border: none; 3 | background-color: transparent; 4 | padding: 0 1rem; 5 | border-radius: 1.2rem; 6 | color: inherit; 7 | cursor: pointer; 8 | font-weight: 500; 9 | font-size: 14px; 10 | /* height: 2.4rem; */ 11 | display: inline-flex; 12 | gap: 0.5rem; 13 | align-items: center; 14 | transition: filter 0.2s ease-out; 15 | } 16 | 17 | .CSVImporter svg { 18 | display: block; 19 | } 20 | 21 | .CSVImporter svg path { 22 | stroke: currentColor !important; 23 | } 24 | 25 | .CSVImporter:hover, 26 | .CSVImporter:active { 27 | filter: brightness(1.2); 28 | } 29 | 30 | .CSVImporter-dialog::backdrop { 31 | background-color: rgba(0, 0, 0, 0.85); 32 | } 33 | 34 | .CSVImporter-dialog { 35 | border-radius: 1rem; 36 | width: 80vw; 37 | height: 100vh; 38 | min-width: 907px; 39 | border: none; 40 | position: fixed; 41 | inset: 0; 42 | padding: 0; 43 | margin: auto; 44 | } 45 | 46 | .CSVImporter-div { 47 | border: none; 48 | display: block; 49 | width: 100%; 50 | height: 98vh; 51 | overflow-y: hidden; 52 | } 53 | 54 | @media (max-width: 768px) { 55 | .CSVImporter-dialog { 56 | width: 90vw; 57 | min-width: 950px; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/i18n/de.ts: -------------------------------------------------------------------------------- 1 | const translations = { 2 | Upload: "Hochladen", 3 | "Select Header": "Kopfzeile auswählen", 4 | "Map Columns": "Spalten zuordnen", 5 | "Expected Column": "Erwartete Spalten", 6 | Required: "Erforderlich", 7 | "Drop your file here": "Datei hier ablegen", 8 | or: "oder", 9 | "Browse files": "Dateien durchsuchen", 10 | "Download Template": "Vorlage herunterladen", 11 | "Only CSV, XLS, and XLSX files can be uploaded": "Nur CSV-, XLS- und XLSX-Dateien können hochgeladen werden", 12 | "Select Header Row": "Kopfzeilenreihe auswählen", 13 | "Select the row which contains the column headers": "Wähle die Zeile, die die Spaltenüberschriften enthält", 14 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Nur das erste Blatt ("{{sheet}}") der Excel-Datei wird importiert. Um mehrere Blätter zu importieren, lade bitte jedes Blatt einzeln hoch.", 15 | "Cancel": "Abbrechen", 16 | "Continue": "Weiter", 17 | "Your File Column": "Deine Spalte der Datei", 18 | "Your Sample Data": "Deine Beispieldaten", 19 | "Destination Column": "Zielspalte", 20 | "Include": "Einfügen", 21 | "Submit": "Senden", 22 | "Loading...": "Laden...", 23 | "Please include all required columns": "Bitte alle erforderlichen Spalten einfügen", 24 | "Back": "Zurück", 25 | "- Select one -": "- Wähle eine aus -", 26 | "- empty -": "- leer -", 27 | "Import Successful": "Import erfolgreich", 28 | "Upload Successful": "Upload erfolgreich", 29 | "Done": "Fertig", 30 | "Upload another file": "Eine weitere Datei hochladen", 31 | }; 32 | 33 | export default translations; 34 | -------------------------------------------------------------------------------- /src/i18n/es.ts: -------------------------------------------------------------------------------- 1 | const translations = { 2 | Upload: "Subir", 3 | "Select Header": "Seleccionar encabezado", 4 | "Map Columns": "Mapear columnas", 5 | "Expected Column": "Columnas esperadas", 6 | Required: "Requerido", 7 | "Drop your file here": "Suelta tu archivo aquí", 8 | or: "o", 9 | "Browse files": "Examinar archivos", 10 | "Download Template": "Descargar plantilla", 11 | "Only CSV, XLS, and XLSX files can be uploaded": "Solo se pueden subir archivos CSV, XLS y XLSX", 12 | "Select Header Row": "Seleccionar fila de encabezado", 13 | "Select the row which contains the column headers": "Selecciona la fila que contiene los encabezados de las columnas", 14 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": " Solo se importará la primera hoja ("{{sheet}}") del archivo de Excel. Para importar varias hojas, sube cada hoja individualmente.", 15 | "Cancel": "Cancelar", 16 | "Continue": "Continuar", 17 | "Your File Column": "Tu columna del archivo", 18 | "Your Sample Data": "Tus datos de muestra", 19 | "Destination Column": "Columna de destino", 20 | "Include": "Incluir", 21 | "Submit": "Enviar", 22 | "Loading...": "Cargando...", 23 | "Please include all required columns": "Por favor incluye todas las columnas requeridas", 24 | "Back": "Atrás", 25 | "- Select one -": "- Selecciona uno -", 26 | "- empty -": "- vacío -", 27 | "Import Successful": "Importación exitosa", 28 | "Upload Successful": "Se ha subido con éxito", 29 | "Done": "Listo", 30 | "Upload another file": "Subir otro archivo", 31 | }; 32 | 33 | export default translations; 34 | -------------------------------------------------------------------------------- /src/i18n/fr.ts: -------------------------------------------------------------------------------- 1 | // Translations in french 2 | //TODO: Double the translations 3 | const translations = { 4 | Upload: "Télécharger", 5 | "Select Header": "Sélectionner l'en-tête", 6 | "Map Columns": "Mapper les colonnes", 7 | "Expected Column": "Colonne attendue", 8 | Required: "Requis", 9 | "Drop your file here": "Déposez votre fichier ici", 10 | or: "ou", 11 | "Browse files": "Parcourir les fichiers", 12 | "Download Template": "Télécharger le modèle", 13 | "Only CSV, XLS, and XLSX files can be uploaded": "Seuls les fichiers CSV, XLS et XLSX peuvent être téléchargés", 14 | "Select Header Row": "Sélectionner la ligne d'en-tête", 15 | "Select the row which contains the column headers": "Sélectionnez la ligne qui contient les en-têtes de colonne", 16 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Seule la première feuille ("{{sheet}}") du fichier Excel sera importée. Pour importer plusieurs feuilles, veuillez télécharger chaque feuille individuellement.", 17 | "Cancel": "Annuler", 18 | "Continue": "Continuer", 19 | "Your File Column": "Votre colonne de fichier", 20 | "Your Sample Data": "Vos données d'échantillon", 21 | "Destination Column": "Colonne de destination", 22 | "Include": "Inclure", 23 | "Submit": "Soumettre", 24 | "Loading...": "Chargement...", 25 | "Please include all required columns": "Veuillez inclure toutes les colonnes requises", 26 | "Back": "Retour", 27 | "- Select one -": "- Sélectionnez un -", 28 | "- empty -": "- vide -", 29 | "Import Successful": "Importation réussie", 30 | "Upload Successful": "Téléchargement réussi", 31 | "Done": "Terminé", 32 | "Upload another file": "Télécharger un autre fichier", 33 | }; 34 | 35 | export default translations; 36 | -------------------------------------------------------------------------------- /src/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18, { Resource } from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import esTranslation from "./es"; 4 | import frTranslation from "./fr"; 5 | import itTranslations from "./it"; 6 | import deTranslations from "./de"; 7 | 8 | const resources: Resource = { 9 | en: { 10 | translation: {}, 11 | }, 12 | fr: { 13 | translation: frTranslation, 14 | }, 15 | es: { 16 | translation: esTranslation, 17 | }, 18 | it: { 19 | translation: itTranslations, 20 | }, 21 | de: { 22 | translation: deTranslations, 23 | }, 24 | }; 25 | 26 | i18.use(initReactI18next).init({ 27 | resources, 28 | lng: "en", 29 | keySeparator: false, 30 | interpolation: { 31 | escapeValue: false, 32 | }, 33 | }); 34 | 35 | export default i18; 36 | -------------------------------------------------------------------------------- /src/i18n/it.ts: -------------------------------------------------------------------------------- 1 | // Translations in Italian 2 | const translations = { 3 | Upload: "Caricare", 4 | "Select Header": "Seleziona intestazione", 5 | "Map Columns": "Mappa colonne", 6 | "Expected Column": "Colonna prevista", 7 | Required: "Richiesto", 8 | "Drop your file here": "Trascina il tuo file qui", 9 | or: "oppure", 10 | "Browse files": "Sfoglia file", 11 | "Download Template": "Scarica il modello", 12 | "Only CSV, XLS, and XLSX files can be uploaded": "Solo i file CSV, XLS e XLSX possono essere caricati", 13 | "Select Header Row": "Seleziona la riga di intestazione", 14 | "Select the row which contains the column headers": "Seleziona la riga che contiene le intestazioni delle colonne", 15 | "Only the first sheet ("{{sheet}}") of the Excel file will be imported. To import multiple sheets, please upload each sheet individually.": "Solo il primo foglio ("{{sheet}}") del file Excel verrà importato. Per importare più fogli, carica ogni foglio singolarmente.", 16 | "Cancel": "Annulla", 17 | "Continue": "Continua", 18 | "Your File Column": "La tua colonna di file", 19 | "Your Sample Data": "I tuoi dati di esempio", 20 | "Destination Column": "Colonna di destinazione", 21 | "Include": "Includere", 22 | "Submit": "Invia", 23 | "Loading...": "Caricamento...", 24 | "Please include all required columns": "Si prega di includere tutte le colonne richieste", 25 | "Back": "Indietro", 26 | "- Select one -": "- Selezionane uno -", 27 | "- empty -": "- vuoto -", 28 | "Import Successful": "Importazione riuscita", 29 | "Upload Successful": "Caricamento riuscito", 30 | "Done": "Fatto", 31 | "Upload another file": "Carica un altro file", 32 | }; 33 | 34 | export default translations; 35 | -------------------------------------------------------------------------------- /src/importer/components/Box/index.tsx: -------------------------------------------------------------------------------- 1 | import classes from "../../utils/classes"; 2 | import { BoxProps } from "./types"; 3 | import style from "./style/Box.module.scss"; 4 | 5 | export default function Box({ className, variants = [], ...props }: BoxProps) { 6 | const variantStyles = classes(variants.map((c: keyof typeof style) => style[c])); 7 | const containerClasses = classes([style.box, variantStyles, className]); 8 | 9 | return
; 10 | } 11 | -------------------------------------------------------------------------------- /src/importer/components/Box/style/Box.module.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | display: block; 3 | margin: 0 auto; 4 | padding: var(--m); 5 | background-color: var(--color-background-modal); 6 | border-radius: var(--border-radius-5); 7 | box-shadow: 0 0 20px var(--color-background-modal-shadow); 8 | max-width: 100%; 9 | 10 | &.fluid { 11 | max-width: none; 12 | } 13 | &.mid { 14 | max-width: 440px; 15 | } 16 | &.wide { 17 | max-width: 660px; 18 | } 19 | &.space-l { 20 | padding: var(--m-l); 21 | } 22 | &.space-mid { 23 | padding: var(--m); 24 | } 25 | &.space-none { 26 | padding: 0; 27 | } 28 | &.bg-shade { 29 | background-color: var(--color-background-modal-shade); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/importer/components/Box/types/index.ts: -------------------------------------------------------------------------------- 1 | export type BoxVariant = "fluid" | "mid" | "wide" | "space-l" | "space-mid" | "space-none" | "bg-shade"; 2 | 3 | export type BoxProps = React.HTMLAttributes & { 4 | variants?: BoxVariant[]; 5 | }; 6 | 7 | export const boxVariants: BoxVariant[] = ["fluid", "mid", "wide", "space-l", "space-mid", "space-none", "bg-shade"]; 8 | -------------------------------------------------------------------------------- /src/importer/components/Checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import classes from "../../utils/classes"; 2 | import { CheckboxProps } from "./types"; 3 | import style from "./style/Checkbox.module.scss"; 4 | 5 | export default function Checkbox({ label, className, ...props }: CheckboxProps) { 6 | const containerClasses = classes([style.container, className]); 7 | 8 | return ( 9 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/importer/components/Checkbox/style/Checkbox.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: inline-block; 3 | gap: var(--m-xs); 4 | align-items: center; 5 | 6 | &:has(input:not(:disabled)) { 7 | cursor: pointer; 8 | } 9 | 10 | input[type="checkbox"] { 11 | -webkit-appearance: none; 12 | appearance: none; 13 | background-color: transparent; 14 | margin: 0; 15 | color: var(--color-primary); 16 | width: var(--m); 17 | height: var(--m); 18 | border: 2px solid var(--color-border); 19 | display: grid; 20 | place-content: center; 21 | border-radius: var(--border-radius-1); 22 | cursor: pointer; 23 | 24 | &::before { 25 | content: ""; 26 | width: var(--m-xs); 27 | height: var(--m-xs); 28 | } 29 | 30 | &:checked { 31 | background-color: var(--color-primary); 32 | border-color: var(--color-primary); 33 | } 34 | 35 | &:checked::before { 36 | clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); 37 | box-shadow: inset 1em 1em var(--color-text-on-primary); 38 | } 39 | 40 | &:not(:disabled) { 41 | &:focus-visible { 42 | outline: 1px solid var(--color-border); 43 | outline-offset: 3px; 44 | } 45 | } 46 | 47 | &:disabled { 48 | --container-color: var(--container-disabled); 49 | color: var(--container-disabled); 50 | cursor: default; 51 | background-color: var(--color-input-disabled); 52 | border-color: var(--color-border-soft); 53 | 54 | &:checked { 55 | &::before { 56 | clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); 57 | box-shadow: inset 1em 1em var(--color-border-soft); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/importer/components/Checkbox/types/index.ts: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes, ReactElement } from "react"; 2 | 3 | export type CheckboxProps = InputHTMLAttributes & { 4 | label?: string | ReactElement; 5 | }; 6 | -------------------------------------------------------------------------------- /src/importer/components/Errors/index.tsx: -------------------------------------------------------------------------------- 1 | import { sizes } from "../../settings/theme"; 2 | import classes from "../../utils/classes"; 3 | import style from "./style/Errors.module.scss"; 4 | import { PiInfo } from "react-icons/pi"; 5 | 6 | export default function Errors({ error, centered = false }: { error?: unknown; centered?: boolean }) { 7 | return error ? ( 8 |
9 |

10 | 11 | {error.toString()} 12 |

13 |
14 | ) : null; 15 | } 16 | -------------------------------------------------------------------------------- /src/importer/components/Errors/style/Errors.module.scss: -------------------------------------------------------------------------------- 1 | .errors { 2 | color: var(--color-text-error); 3 | margin: var(--m-xxs) 0; 4 | 5 | p { 6 | margin: 0; 7 | display: flex; 8 | align-items: center; 9 | gap: var(--m-xxs); 10 | text-align: left; 11 | } 12 | } 13 | 14 | .centered { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | flex-direction: column; 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /src/importer/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | import { Portal } from "@chakra-ui/react"; 3 | import useClickOutside from "../../hooks/useClickOutside"; 4 | import useRect from "../../hooks/useRect"; 5 | import useWindowSize from "../../hooks/useWindowSize"; 6 | import classes from "../../utils/classes"; 7 | import { InputProps } from "./types"; 8 | import style from "./style/Input.module.scss"; 9 | import { PiCaretDown, PiInfo } from "react-icons/pi"; 10 | 11 | export default function Input({ as = "input", label, icon, iconAfter, error, options, className, variants = [], children, ...props }: InputProps) { 12 | const Element = as; 13 | 14 | const variantStyles = classes(variants.map((c: keyof typeof style) => style[c])); 15 | 16 | const containerClassName = classes([style.container, variantStyles, className]); 17 | 18 | const icon1 = icon && {icon}; 19 | 20 | const icon2 = iconAfter ? ( 21 | {iconAfter} 22 | ) : ( 23 | error && ( 24 | 25 | 26 | 27 | ) 28 | ); 29 | 30 | const iconSelect = options && ( 31 | 32 | 33 | 34 | ); 35 | 36 | const selectElement = options && options && 106 | 107 |
108 | 109 | {open && ( 110 |
111 |
112 | {placeholder && ( 113 | 116 | )} 117 | {Object.keys(options).map((k, i) => ( 118 | 127 | ))} 128 |
129 |
130 | )} 131 | 132 | ); 133 | } 134 | -------------------------------------------------------------------------------- /src/importer/components/Input/style/Input.module.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins.scss"; 2 | 3 | .container { 4 | margin-bottom: var(--m); 5 | text-align: left; 6 | 7 | &:only-child { 8 | margin: var(--m-xxs) 0; 9 | } 10 | 11 | &:has(input[disabled]) { 12 | .label { 13 | cursor: default; 14 | } 15 | } 16 | 17 | & > label { 18 | font: inherit; 19 | color: inherit; 20 | font-weight: inherit; 21 | line-height: inherit; 22 | background-color: transparent; 23 | border: none; 24 | text-align: inherit; 25 | padding: 0; 26 | font-size: inherit; 27 | accent-color: var(--color-primary); 28 | } 29 | } 30 | 31 | .label { 32 | display: inline-block; 33 | margin-bottom: var(--m-xxs); 34 | cursor: pointer; 35 | line-height: 1; 36 | font-weight: 500; 37 | font-family: var(--font-family-1); 38 | overflow: hidden; 39 | white-space: nowrap; 40 | text-overflow: ellipsis; 41 | } 42 | 43 | .inputWrapper { 44 | background-color: var(--color-input-background); 45 | border-radius: var(--border-radius-2); 46 | outline: 1px solid transparent; 47 | padding: calc(var(--m-xxs) / 2); 48 | border: 1px solid var(--color-border); 49 | font-weight: 400; 50 | display: flex; 51 | align-items: center; 52 | min-width: 270px; 53 | position: relative; 54 | 55 | &:focus-within, 56 | &:has(.open) { 57 | outline-color: var(--color-text); 58 | z-index: 11; 59 | 60 | .icon svg:not([data-stroke="no-stroke"]) path { 61 | stroke: var(--color-text); 62 | } 63 | 64 | .options { 65 | display: flex; 66 | } 67 | } 68 | 69 | &:has(> [error]), 70 | &.hasError { 71 | border-color: var(--color-text-error); 72 | 73 | .icon svg:not([data-stroke="no-stroke"]) path { 74 | stroke: var(--color-text-error); 75 | } 76 | } 77 | 78 | &:has(> [disabled]) { 79 | background-color: var(--color-input-disabled); 80 | color: var(--color-input-text-disabled); 81 | border-color: var(--color-border-soft); 82 | 83 | .icon.dropdownIcon { 84 | cursor: default; 85 | } 86 | } 87 | 88 | .icon { 89 | padding: 0 var(--m-xxxs); 90 | cursor: pointer; 91 | display: flex; 92 | 93 | button { 94 | background: none; 95 | padding: 0; 96 | border: none; 97 | cursor: pointer; 98 | } 99 | 100 | svg:not([data-stroke="no-stroke"]) path { 101 | display: block; 102 | stroke: var(--color-text-soft); 103 | } 104 | } 105 | 106 | input, 107 | select, 108 | textarea { 109 | -webkit-tap-highlight-color: transparent; 110 | appearance: none; 111 | resize: none; 112 | width: 100%; 113 | display: block; 114 | padding: calc(var(--m-xxs) / 2); 115 | border: none; 116 | background-color: transparent; 117 | flex-grow: 1; 118 | color: var(--color-text); 119 | font: inherit; 120 | font-weight: inherit; 121 | line-height: inherit; 122 | accent-color: var(--color-primary); 123 | 124 | @include placeholder { 125 | color: var(--color-text-soft); 126 | } 127 | 128 | option:disabled { 129 | color: var(--color-text-soft); 130 | } 131 | option:not([value]) { 132 | display: none; 133 | } 134 | 135 | &:focus { 136 | outline: none; 137 | } 138 | } 139 | 140 | select option { 141 | background-color: var(--color-input-background); 142 | border: none; 143 | padding: var(--m); 144 | } 145 | 146 | .fluid & { 147 | min-width: auto; 148 | } 149 | 150 | .small & { 151 | padding: 0 var(--m-xxxs); 152 | min-width: auto; 153 | border-radius: var(--border-radius-1); 154 | 155 | .options { 156 | border-radius: var(--border-radius-1); 157 | 158 | .option { 159 | border-radius: var(--border-radius-1); 160 | } 161 | } 162 | } 163 | 164 | .optionsRef { 165 | display: flex; 166 | position: absolute; 167 | top: 100%; 168 | right: 0; 169 | left: 0; 170 | } 171 | 172 | .select:not([disabled]) { 173 | cursor: pointer; 174 | } 175 | } 176 | 177 | .options { 178 | position: fixed; 179 | top: 100%; 180 | right: 0; 181 | left: 0; 182 | z-index: 1000; 183 | 184 | .inner { 185 | display: flex; 186 | background-color: var(--color-input-background); 187 | border-radius: var(--border-radius-2); 188 | padding: var(--m-xxxxs); 189 | outline: 1px solid var(--color-border); 190 | display: flex; 191 | flex-direction: column; 192 | gap: var(--m-xxxxs); 193 | max-height: calc(100vh - 8px); 194 | overflow-y: auto; 195 | margin-top: var(--m-xxxxs); 196 | width: 100%; 197 | } 198 | 199 | .option { 200 | padding: var(--m-xxs) var(--m-xxs); 201 | border-radius: var(--border-radius-2); 202 | background-color: transparent; 203 | border: none; 204 | 205 | &[disabled] { 206 | color: var(--color-text-soft); 207 | } 208 | 209 | &:not([disabled]) { 210 | cursor: pointer; 211 | 212 | &:hover, 213 | &:focus, 214 | &.selected { 215 | background-color: var(--color-background-menu-hover); 216 | } 217 | &.placeholder { 218 | color: var(--color-text-soft); 219 | } 220 | } 221 | .requiredMark { 222 | color: var(--color-text-error); 223 | } 224 | } 225 | } 226 | 227 | .error { 228 | color: var(--color-text-error); 229 | margin-top: var(--m-xxs); 230 | line-height: 1; 231 | font-weight: 400; 232 | } 233 | 234 | .footer { 235 | color: var(--color-text-soft); 236 | margin-top: var(--m-xxs); 237 | line-height: 1; 238 | font-weight: 400; 239 | } 240 | -------------------------------------------------------------------------------- /src/importer/components/Input/style/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin optional-at-root($sel) { 2 | @at-root #{if(not &, $sel, selector-append(&, $sel))} { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin placeholder { 8 | @include optional-at-root("::-webkit-input-placeholder") { 9 | @content; 10 | } 11 | 12 | @include optional-at-root(":-moz-placeholder") { 13 | @content; 14 | } 15 | 16 | @include optional-at-root("::-moz-placeholder") { 17 | @content; 18 | } 19 | 20 | @include optional-at-root(":-ms-input-placeholder") { 21 | @content; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/importer/components/Input/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, InputHTMLAttributes, ReactElement } from "react"; 2 | 3 | export type inputTypes = 4 | | "date" 5 | | "datetime-local" 6 | | "email" 7 | | "file" 8 | | "month" 9 | | "number" 10 | | "password" 11 | | "search" 12 | | "tel" 13 | | "text" 14 | | "time" 15 | | "url" 16 | | "week"; 17 | 18 | export type InputVariants = "fluid" | "small"; 19 | export type InputOption = ButtonHTMLAttributes & { required?: boolean }; 20 | 21 | export type InputProps = InputHTMLAttributes & 22 | InputHTMLAttributes & 23 | InputHTMLAttributes & { 24 | as?: "input" | "textarea"; 25 | label?: string | ReactElement; 26 | icon?: ReactElement; 27 | iconAfter?: ReactElement; 28 | error?: string; 29 | options?: { [key: string]: InputOption }; 30 | variants?: InputVariants[]; 31 | type?: inputTypes; 32 | }; 33 | -------------------------------------------------------------------------------- /src/importer/components/Portal/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactPortal, useEffect, useState } from "react"; 2 | import { createPortal } from "react-dom"; 3 | import { PortalProps } from "./types"; 4 | 5 | export default function Portal({ children, className = "root-portal", el = "div" }: PortalProps): ReactPortal { 6 | const [container] = useState(() => { 7 | // This will be executed only on the initial render 8 | // https://reactjs.org/docs/hooks-reference.html#lazy-initial-state 9 | return document.createElement(el); 10 | }); 11 | 12 | useEffect(() => { 13 | container.classList.add(className); 14 | container.setAttribute("role", "complementary"); 15 | container.setAttribute("aria-label", "Notifications"); 16 | document.body.appendChild(container); 17 | return () => { 18 | document.body.removeChild(container); 19 | }; 20 | }, []); 21 | 22 | return createPortal(children, container); 23 | } 24 | -------------------------------------------------------------------------------- /src/importer/components/Portal/types/index.ts: -------------------------------------------------------------------------------- 1 | export type PortalProps = React.PropsWithChildren<{ 2 | className?: string; 3 | el?: string; 4 | }>; 5 | -------------------------------------------------------------------------------- /src/importer/components/Stepper/hooks/useStepper.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from "react"; 2 | import { Step, StepperProps } from "../types"; 3 | 4 | export default function useStepper(steps: Step[], initialStep = 0, skipHeader: boolean): StepperProps { 5 | const [current, setCurrent] = useState(initialStep); 6 | 7 | const step = useMemo(() => steps[current], [current, steps]); 8 | 9 | return { steps, current, step, setCurrent, skipHeader }; 10 | } 11 | -------------------------------------------------------------------------------- /src/importer/components/Stepper/index.tsx: -------------------------------------------------------------------------------- 1 | import { colors } from "../../settings/theme"; 2 | import classes from "../../utils/classes"; 3 | import { StepperProps } from "./types"; 4 | import style from "./style/Stepper.module.scss"; 5 | import { PiCheckBold } from "react-icons/pi"; 6 | 7 | export default function Stepper({ steps, current, clickable, setCurrent, skipHeader }: StepperProps) { 8 | return ( 9 |
10 | {steps.map((step, i) => { 11 | if (step.disabled) return null; 12 | const done = i < current; 13 | 14 | const Element = clickable ? "button" : "div"; 15 | 16 | const buttonProps: any = clickable 17 | ? { 18 | onClick: () => setCurrent(i), 19 | type: "button", 20 | } 21 | : {}; 22 | 23 | let displayNumber = i + 1; 24 | if (skipHeader && displayNumber > 1) { 25 | displayNumber--; 26 | } 27 | 28 | return ( 29 | 33 |
{done ? : displayNumber}
34 |
{step.label}
35 |
36 | ); 37 | })} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/importer/components/Stepper/style/Stepper.module.scss: -------------------------------------------------------------------------------- 1 | $transition: all 0.3s ease-out; 2 | 3 | .stepper { 4 | display: flex; 5 | gap: var(--m); 6 | margin: var(--m-xs) auto; 7 | justify-content: center; 8 | 9 | .step { 10 | display: flex; 11 | gap: var(--m-xxs); 12 | align-items: center; 13 | transition: $transition; 14 | 15 | .badge { 16 | border-radius: 50%; 17 | border: 1px solid var(--color-border); 18 | aspect-ratio: 1; 19 | width: 2em; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | transition: $transition; 24 | } 25 | 26 | &.active { 27 | color: var(--color-primary); 28 | 29 | .badge { 30 | background-color: var(--color-primary); 31 | color: var(--color-text-on-primary); 32 | border: none; 33 | } 34 | } 35 | &.done { 36 | .badge { 37 | border-color: var(--color-primary); 38 | } 39 | } 40 | 41 | &:not(:first-of-type) { 42 | &:before { 43 | content: ""; 44 | height: 1px; 45 | width: calc(min(140px, 4vw)); 46 | background-color: var(--color-border); 47 | border-radius: 2px; 48 | margin-right: var(--m-xs); 49 | } 50 | } 51 | } 52 | 53 | .stepWide { 54 | &:not(:first-of-type) { 55 | &:before { 56 | width: calc(min(120px, 10vw)); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/importer/components/Stepper/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Step = { 2 | label: string; 3 | id?: string | number; 4 | disabled?: boolean; 5 | }; 6 | 7 | export type StepperProps = { 8 | steps: Step[]; 9 | current: number; 10 | setCurrent: (step: number) => void; 11 | step: Step; 12 | clickable?: boolean; 13 | skipHeader: boolean; 14 | }; 15 | -------------------------------------------------------------------------------- /src/importer/components/Table/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from "react"; 2 | import classes from "../../utils/classes"; 3 | import { CellProps, RowProps, TableProps } from "./types"; 4 | import themeDefault from "./style/Default.module.scss"; 5 | import Tooltip from "../Tooltip"; 6 | 7 | const TableContext = createContext({}); 8 | 9 | export default function Table({ 10 | data, // An array of objects with the data to be displayed 11 | keyAsId = "id", // Has to be a unique property in the data array to be used as key 12 | theme, // A CSS module object to style the table 13 | mergeThemes, // Should 'theme' be the only style applied (false) or be merged with the default style (true) 14 | highlightColumns, // Columns that should use the highlighted style 15 | hideColumns = ["id"], // Array of columns to be hidden in the display 16 | emptyState, 17 | heading, 18 | background = "zebra", 19 | columnWidths = [], 20 | columnAlignments = [], 21 | fixHeader = false, 22 | onRowClick, 23 | }: TableProps): React.ReactElement { 24 | // THEME 25 | // Tables receive a full CSS module as theme or applies default styles 26 | // depending on mergeThemes being true it will merge both themes or use only the custom one 27 | // use a copy of ./style/Default.module.scss as base to make a custom theme 28 | // another example of the theme lives in src/features/contents/versions/style/TableTheme.module.scss 29 | 30 | const style = !theme ? themeDefault : mergeThemes ? { ...themeDefault, ...theme } : theme; 31 | 32 | // TABLE HEADINGS 33 | // Hide column title if the item has an action (action button) or the title starts with underscore 34 | const modelDatum = data?.[0]; 35 | const thead: any = modelDatum 36 | ? Object.keys(modelDatum).map((k) => { 37 | const value = modelDatum[k]; 38 | 39 | if (k.indexOf("_") === 0) { 40 | return ""; 41 | } 42 | 43 | if (typeof value === "object" && value?.captionInfo) { 44 | return { key: k, captionInfo: value.captionInfo }; 45 | } 46 | 47 | return k; 48 | }) 49 | : {}; 50 | const context = { 51 | style, 52 | highlightColumns, 53 | hideColumns, 54 | columnWidths, 55 | columnAlignments, 56 | }; 57 | 58 | if (!data || !data?.length) return
{emptyState || null}
; 59 | 60 | const tableStyle = classes([style?.table, style?.[background], fixHeader && style?.fixHeader]); 61 | 62 | const headingContent = heading ? ( 63 |
{heading}
64 | ) : ( 65 |
66 | 67 |
68 | ); 69 | 70 | return ( 71 | 72 |
73 | {headingContent} 74 |
75 | {data.map((d, i) => { 76 | const key = keyAsId && d?.[keyAsId] ? d[keyAsId] : i; 77 | const props = { datum: d, onClick: onRowClick }; 78 | return ; 79 | })} 80 |
81 |
82 | {!data.length && ( 83 |
84 |

Empty

85 |
86 | )} 87 |
88 | ); 89 | } 90 | 91 | const Row = ({ datum, onClick, isHeading }: RowProps) => { 92 | const { style, highlightColumns, hideColumns, columnWidths, columnAlignments } = useContext(TableContext); 93 | 94 | const className = classes([style?.tr]); 95 | return ( 96 |
onClick?.(datum)}> 97 | {Object.keys(datum) 98 | .filter((k) => !hideColumns.includes(datum[k]) && !hideColumns.includes(k)) 99 | .map((k, i) => { 100 | // datum is the row 101 | // datum[k] is the content for the cell 102 | // If it is an object with the 'content' property, use that as content (can be JSX or a primitive) 103 | // Another 'raw' property with a primitive value is used to sort and search 104 | let content = (datum[k] as any)?.content || datum[k]; 105 | const tooltip = (datum[k] as any)?.tooltip; 106 | const captionInfo = isHeading ? (datum[k] as any)?.captionInfo : null; 107 | const headingKey = isHeading ? (datum[k] as any)?.key : null; 108 | content = isHeading && captionInfo ? {headingKey} : content; 109 | const wrappedContent = content && typeof content === "string" ? {content} : content; 110 | 111 | const cellClass = classes([ 112 | highlightColumns?.includes(k) && style.highlight, 113 | !wrappedContent && style.empty, 114 | typeof content !== "string" && style.element, 115 | ]); 116 | 117 | const cellStyle = { width: columnWidths?.[i] || "auto", textAlign: columnAlignments?.[i] || "left" }; 118 | 119 | return ( 120 | 121 | {wrappedContent} 122 | 123 | ); 124 | })} 125 |
126 | ); 127 | }; 128 | 129 | const Cell = ({ children, cellClass, cellStyle, tooltip }: CellProps) => { 130 | const { style } = useContext(TableContext); 131 | const cellProps = { 132 | className: classes([style?.td, cellClass, !children && style?.empty]), 133 | role: "cell", 134 | style: cellStyle, 135 | ...(tooltip ? { title: tooltip } : {}), 136 | }; 137 | return
{children}
; 138 | }; 139 | -------------------------------------------------------------------------------- /src/importer/components/Table/storyData.ts: -------------------------------------------------------------------------------- 1 | const storyData = [ 2 | { 3 | id: 1, 4 | Name: { 5 | raw: "John Doe", 6 | content: "John Doe", 7 | captionInfo: "This is a caption example", 8 | }, 9 | Age: 25, 10 | Email: "john.doe@example.com", 11 | }, 12 | { 13 | id: 2, 14 | Name: { 15 | raw: "Huge line", 16 | content: 17 | "Huge line with overflow. Lorem ipsum dolor sit amet. Aequam memento rebus in arduis servare mentem. Ubi fini saeculi fortunae comutatione supersunt in elipse est.", 18 | tooltip: 19 | "Huge line with overflow. Lorem ipsum dolor sit amet. Aequam memento rebus in arduis servare mentem. Ubi fini saeculi fortunae comutatione supersunt in elipse est.", 20 | }, 21 | Age: 30, 22 | Email: "jane.smith@example.com", 23 | }, 24 | { 25 | id: 3, 26 | Name: "Mike Johnson", 27 | Age: 28, 28 | Email: "mike.johnson@example.com", 29 | }, 30 | { 31 | id: 4, 32 | Name: "Emily Davis", 33 | Age: 32, 34 | Email: "emily.davis@example.com", 35 | }, 36 | { 37 | id: 5, 38 | Name: "Alex Wilson", 39 | Age: 27, 40 | Email: "alex.wilson@example.com", 41 | }, 42 | { 43 | id: 6, 44 | Name: "Sarah Thompson", 45 | Age: 29, 46 | Email: "sarah.thompson@example.com", 47 | }, 48 | { 49 | id: 7, 50 | Name: "Daniel Anderson", 51 | Age: 31, 52 | Email: "daniel.anderson@example.com", 53 | }, 54 | { 55 | id: 8, 56 | Name: "Michelle Brown", 57 | Age: 26, 58 | Email: "michelle.brown@example.com", 59 | }, 60 | { 61 | id: 9, 62 | Name: "Robert Taylor", 63 | Age: 33, 64 | Email: "robert.taylor@example.com", 65 | }, 66 | { 67 | id: 10, 68 | Name: "Laura Miller", 69 | Age: 28, 70 | Email: "laura.miller@example.com", 71 | }, 72 | { 73 | id: 11, 74 | Name: "Michael Johnson", 75 | Age: 35, 76 | email: "michael.johnson@example.com", 77 | }, 78 | { 79 | id: 12, 80 | Name: "Jessica Davis", 81 | Age: 27, 82 | email: "jessica.davis@example.com", 83 | }, 84 | { 85 | id: 13, 86 | Name: "Andrew Smith", 87 | Age: 32, 88 | email: "andrew.smith@example.com", 89 | }, 90 | { 91 | id: 14, 92 | Name: "Emily Wilson", 93 | Age: 29, 94 | email: "emily.wilson@example.com", 95 | }, 96 | { 97 | id: 15, 98 | Name: "David Anderson", 99 | Age: 33, 100 | email: "david.anderson@example.com", 101 | }, 102 | { 103 | id: 16, 104 | Name: "Sophia Brown", 105 | Age: 28, 106 | email: "sophia.brown@example.com", 107 | }, 108 | { 109 | id: 17, 110 | Name: "Matthew Taylor", 111 | Age: 31, 112 | email: "matthew.taylor@example.com", 113 | }, 114 | { 115 | id: 18, 116 | Name: "Olivia Johnson", 117 | Age: 26, 118 | email: "olivia.johnson@example.com", 119 | }, 120 | { 121 | id: 19, 122 | Name: "James Davis", 123 | Age: 30, 124 | email: "james.davis@example.com", 125 | }, 126 | { 127 | id: 20, 128 | Name: "Grace Smith", 129 | Age: 27, 130 | email: "grace.smith@example.com", 131 | }, 132 | ]; 133 | 134 | export default storyData; 135 | -------------------------------------------------------------------------------- /src/importer/components/Table/style/Default.module.scss: -------------------------------------------------------------------------------- 1 | $radius: var(--border-radius-2); 2 | $cellHeight: 44px; 3 | 4 | .table { 5 | display: flex; 6 | flex-direction: column; 7 | flex: 1 1 auto; 8 | width: 100%; 9 | border-collapse: collapse; 10 | border-spacing: 0; 11 | border-radius: $radius; 12 | outline: 1px solid var(--color-border); 13 | table-layout: fixed; 14 | overflow: hidden; 15 | 16 | .thead { 17 | display: table-header-group; 18 | } 19 | 20 | .tbody { 21 | display: block; 22 | overflow: auto; 23 | width: 100%; 24 | } 25 | 26 | .tr { 27 | display: flex; 28 | width: 100%; 29 | overflow: hidden; 30 | flex-wrap: nowrap; 31 | } 32 | 33 | .td { 34 | display: inline-flex; 35 | align-items: center; 36 | height: $cellHeight; 37 | flex-shrink: 0; 38 | } 39 | 40 | .caption { 41 | display: table-caption; 42 | border-bottom: 1px solid var(--color-border); 43 | background-color: var(--color-background-modal); 44 | padding: 0 var(--m-s); 45 | border-radius: var(--border-radius-2) var(--border-radius-2) 0 0; 46 | } 47 | 48 | .thead { 49 | .tr { 50 | .td { 51 | font-weight: 400; 52 | white-space: nowrap; 53 | padding: var(--m-xxxs) var(--m-s); 54 | border-bottom: 1px solid var(--color-border); 55 | color: var(--color-text-secondary); 56 | 57 | &:first-of-type { 58 | border-radius: $radius 0 0 0; 59 | } 60 | 61 | &:last-child { 62 | border-radius: 0 $radius 0 0; 63 | } 64 | } 65 | } 66 | } 67 | 68 | .tbody { 69 | .tr { 70 | .td { 71 | vertical-align: middle; 72 | padding: var(--m-xxxs) var(--m-s); 73 | font-weight: 400; 74 | 75 | & > span, 76 | & > small { 77 | text-overflow: ellipsis; 78 | white-space: nowrap; 79 | display: block; 80 | overflow: hidden; 81 | } 82 | 83 | &.highlight { 84 | font-weight: 500; 85 | } 86 | 87 | &.element { 88 | padding: 0 var(--m-s); 89 | } 90 | } 91 | 92 | &:hover { 93 | box-shadow: 0 0 0 2px var(--color-border); 94 | position: relative; 95 | } 96 | 97 | &:last-child { 98 | border-radius: 0 0 $radius $radius; 99 | 100 | .td { 101 | border-bottom-color: transparent; 102 | 103 | &:first-of-type { 104 | border-radius: 0 0 0 $radius; 105 | } 106 | 107 | &:last-child { 108 | border-radius: 0 0 $radius 0; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | &.zebra { 116 | background-color: var(--color-background-modal); 117 | 118 | .tbody { 119 | .tr { 120 | &:nth-child(odd) { 121 | .td { 122 | background-color: var(--color-background); 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | &.dark { 130 | background-color: var(--color-background); 131 | 132 | .thead .tr .td { 133 | background-color: var(--color-background-modal); 134 | } 135 | } 136 | 137 | &.light { 138 | background-color: var(--color-background-modal); 139 | } 140 | 141 | &.transparent { 142 | .thead { 143 | background-color: var(--color-background-modal); 144 | } 145 | .tbody { 146 | .tr { 147 | &:hover { 148 | box-shadow: none; 149 | position: static; 150 | } 151 | } 152 | } 153 | } 154 | 155 | &.dark, 156 | &.light { 157 | .tbody { 158 | .tr { 159 | &:hover { 160 | box-shadow: none; 161 | position: static; 162 | } 163 | 164 | &:first-of-type .td { 165 | padding-top: var(--m-s); 166 | height: calc(#{$cellHeight} + var(--m-s) - var(--m-xxxs)); 167 | } 168 | 169 | &:last-of-type .td { 170 | padding-bottom: var(--m-s); 171 | height: calc(#{$cellHeight} + var(--m-s) - var(--m-xxxs)); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | .emptyMsg { 179 | display: flex; 180 | justify-content: center; 181 | align-items: center; 182 | margin-top: 15vh; 183 | text-align: center; 184 | } 185 | -------------------------------------------------------------------------------- /src/importer/components/Table/types/index.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren, ReactElement } from "react"; 2 | 3 | type Style = { readonly [key: string]: string }; 4 | 5 | type Primitive = string | number | boolean | null | undefined; 6 | 7 | export type TableComposite = { 8 | raw: Primitive; 9 | content: Primitive | React.ReactElement; 10 | tooltip?: string; 11 | captionInfo?: string; 12 | }; 13 | 14 | export type TableValue = Primitive | TableComposite; 15 | 16 | export type TableDatum = { 17 | [key: string]: TableValue; 18 | }; 19 | 20 | export type TableData = TableDatum[]; 21 | 22 | export type TableProps = { 23 | data: TableData; 24 | keyAsId?: string; 25 | theme?: Style; 26 | mergeThemes?: boolean; 27 | highlightColumns?: string[]; 28 | hideColumns?: string[]; 29 | emptyState?: ReactElement; 30 | heading?: ReactElement; 31 | background?: "zebra" | "dark" | "light" | "transparent"; 32 | columnWidths?: string[]; 33 | columnAlignments?: ("left" | "center" | "right" | "")[]; 34 | fixHeader?: boolean; 35 | onRowClick?: (row: TableDatum) => void; 36 | }; 37 | 38 | export type RowProps = { 39 | datum: TableDatum; 40 | isHeading?: boolean; 41 | onClick?: (row: TableDatum) => void; 42 | }; 43 | 44 | export type CellProps = PropsWithChildren<{ 45 | cellClass?: string; 46 | cellStyle: Style; 47 | tooltip?: string; 48 | }>; 49 | -------------------------------------------------------------------------------- /src/importer/components/ToggleFilter/ToggleFilter.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ToggleFilter from './index'; 3 | 4 | export default { 5 | title: 'User Interface/ToggleFilter', 6 | component: ToggleFilter, 7 | argTypes: { 8 | options: { 9 | control: { 10 | type: 'object' 11 | } 12 | }, 13 | onChange: { action: 'changed' } 14 | } 15 | }; 16 | 17 | export const Default = () => ( 18 | console.log(option)} 25 | /> 26 | ); 27 | 28 | export const WithCustomOptions = () => ( 29 | console.log(option)} 35 | /> 36 | ); 37 | -------------------------------------------------------------------------------- /src/importer/components/ToggleFilter/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import classes from "../../utils/classes"; 3 | import { Option, ToggleFilterProps } from "./types"; 4 | import style from "./style/ToggleFilter.module.scss"; 5 | 6 | function ToggleFilter({ options, onChange, className }: ToggleFilterProps) { 7 | const [selectedOption, setSelectedOption] = useState(null); 8 | const toggleFilterClassName = classes([style.toggleFilter, className]); 9 | 10 | useEffect(() => { 11 | const defaultSelected = options.find((option) => option.selected); 12 | setSelectedOption(defaultSelected ? defaultSelected.label : options[0]?.label); 13 | }, [options]); 14 | 15 | const handleClick = (option: Option) => { 16 | setSelectedOption(option.label); 17 | if (onChange) { 18 | onChange(option.filterValue); 19 | } 20 | }; 21 | 22 | const getOptionColor = (option: Option) => { 23 | if (option.color) { 24 | return option.color; 25 | } 26 | return selectedOption === option.label ? "var(--color-tertiary)" : "var(--color-text)"; 27 | }; 28 | 29 | return ( 30 |
31 | {options.map((option) => ( 32 | 43 | ))} 44 |
45 | ); 46 | } 47 | 48 | export default ToggleFilter; 49 | -------------------------------------------------------------------------------- /src/importer/components/ToggleFilter/style/ToggleFilter.module.scss: -------------------------------------------------------------------------------- 1 | .toggleFilter { 2 | display: flex; 3 | align-items: center; 4 | background-color: var(--color-input-background); 5 | border-radius: 20px; 6 | overflow: hidden; 7 | min-height: 36px; 8 | } 9 | 10 | .toggleOption { 11 | padding: 8px 16px; 12 | cursor: pointer; 13 | 14 | &.selected { 15 | background-color: var(--color-text-on-tertiary); 16 | border-radius: 20px; 17 | transition: background-color 0.2s, color 0.2s; 18 | } 19 | 20 | .defaultColor { 21 | color: var(--color-text); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/importer/components/ToggleFilter/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Option { 2 | label: string; 3 | filterValue: string, 4 | selected: boolean; 5 | color?: string; 6 | } 7 | 8 | export interface ToggleFilterProps { 9 | options: Option[]; 10 | className?: string; 11 | onChange: (option: string) => void; 12 | } -------------------------------------------------------------------------------- /src/importer/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import classes from "../../utils/classes"; 4 | import getStringLengthOfChildren from "../../utils/getStringLengthOfChildren"; 5 | import { AsMap, TooltipProps } from "./types"; 6 | import style from "./style/Tooltip.module.scss"; 7 | import { PiInfo } from "react-icons/pi"; 8 | 9 | export default function Tooltip({ as, className, title, children, icon = , ...props }: TooltipProps) { 10 | const Tag: any = as || "span"; 11 | 12 | const length = getStringLengthOfChildren(title); 13 | const wrapperClasses = classes([style.tooltip, className, length > 30 && style.multiline]); 14 | 15 | const [tooltipVisible, setTooltipVisible] = useState(false); 16 | const [position, setPosition] = useState({ top: 0, left: 0 }); 17 | const targetRef = useRef(null); 18 | 19 | // Create a ref to attach the tooltip portal to 20 | const tooltipContainer = useRef(document.createElement("div")); 21 | 22 | useEffect(() => { 23 | // Appending the tooltip container to the body on mount 24 | document.body.appendChild(tooltipContainer.current); 25 | 26 | // Removing the tooltip container from the body on unmount 27 | return () => { 28 | document.body.removeChild(tooltipContainer.current); 29 | }; 30 | }, []); 31 | 32 | const showTooltip = () => { 33 | if (targetRef.current) { 34 | const rect = targetRef.current.getBoundingClientRect(); 35 | setPosition({ 36 | top: rect.bottom + window.scrollY, 37 | left: rect.left + rect.width / 2 + window.scrollX, 38 | }); 39 | setTooltipVisible(true); 40 | } 41 | }; 42 | 43 | const hideTooltip = () => { 44 | setTooltipVisible(false); 45 | }; 46 | 47 | const tooltipMessage = tooltipVisible && ( 48 | 49 | {title} 50 | 51 | ); 52 | 53 | return ( 54 | 55 | {children} 56 | 57 | {icon} 58 | {tooltipMessage} 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/importer/components/Tooltip/style/Tooltip.module.scss: -------------------------------------------------------------------------------- 1 | $side: var(--m-xxxs); 2 | $height: calc($side * 1.732); 3 | 4 | .tooltip { 5 | display: inline-flex; 6 | align-items: center; 7 | gap: var(--m-xs); 8 | 9 | .icon { 10 | position: relative; 11 | display: block; 12 | cursor: pointer; 13 | } 14 | 15 | &.multiline .message { 16 | width: 260px; 17 | white-space: normal; 18 | } 19 | } 20 | 21 | .message { 22 | position: absolute; 23 | transform: translateX(-50%); 24 | background-color: var(--color-background-modal); 25 | z-index: 3; 26 | padding: var(--m-xxs) var(--m-xs); 27 | border-radius: var(--border-radius); 28 | margin-top: var(--m-xs); 29 | box-shadow: 0 0 0 1px var(--color-border), 0 5px 15px rgba(0, 0, 0, 0.2); 30 | max-width: 300px; 31 | 32 | &::after, 33 | &::before { 34 | position: absolute; 35 | top: calc($height * -1); 36 | left: 50%; 37 | border-left: $side solid transparent; 38 | border-right: $side solid transparent; 39 | border-bottom: $height solid var(--color-border); 40 | content: ""; 41 | font-size: 0; 42 | line-height: 0; 43 | width: 0; 44 | transform: translateX(-50%); 45 | } 46 | 47 | &::after { 48 | top: calc($height * -1 + 2px); 49 | border-bottom: $height solid var(--color-background-modal); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/importer/components/Tooltip/types/index.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export type AsMap = { 4 | div: React.HTMLProps; 5 | span: React.HTMLProps; 6 | p: React.HTMLProps; 7 | }; 8 | 9 | export type TooltipProps = { 10 | as?: T; 11 | title?: string | ReactNode; 12 | icon?: ReactNode; 13 | } & AsMap[T]; 14 | -------------------------------------------------------------------------------- /src/importer/components/UploaderWrapper/UploaderWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useDropzone } from "react-dropzone"; 3 | import { useTranslation } from "react-i18next"; 4 | import { Button } from "@chakra-ui/button"; 5 | import { Box, Text } from "@chakra-ui/react"; 6 | import useThemeStore from "../../stores/theme"; 7 | import { UploaderWrapperProps } from "./types"; 8 | import { PiArrowCounterClockwise, PiFile } from "react-icons/pi"; 9 | 10 | export default function UploaderWrapper({ onSuccess, setDataError, ...props }: UploaderWrapperProps) { 11 | const [loading, setLoading] = useState(false); 12 | const theme = useThemeStore((state) => state.theme); 13 | const { t } = useTranslation(); 14 | 15 | const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ 16 | noClick: true, 17 | noKeyboard: true, 18 | maxFiles: 1, 19 | // maxSize: 1 * Math.pow(1024, 3), 20 | accept: { 21 | "application/vnd.ms-excel": [".xls"], 22 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], 23 | "text/csv": [".csv"], 24 | }, 25 | onDropRejected: (fileRejections) => { 26 | setLoading(false); 27 | // const errorMessage = fileRejections.map((fileRejection) => fileRejection.errors[0].message).join(", "); 28 | const errorMessage = fileRejections[0].errors[0].message; 29 | setDataError(errorMessage); 30 | }, 31 | onDropAccepted: async ([file]) => { 32 | setLoading(true); 33 | onSuccess(file); 34 | setLoading(false); 35 | }, 36 | }); 37 | 38 | return ( 39 | 40 | 51 | 52 | {isDragActive ? ( 53 | {t("Drop your file here")} 54 | ) : loading ? ( 55 | {t("Loading...")} 56 | ) : ( 57 | <> 58 | {t("Drop your file here")} 59 | {t("or")} 60 | 76 | 77 | )} 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/importer/components/UploaderWrapper/style/uppy.overrides.scss: -------------------------------------------------------------------------------- 1 | .uppy-Container.uppy-Container { 2 | flex-grow: 1; 3 | 4 | .uppy { 5 | &-Root { 6 | font-family: var(--font-family-1); 7 | height: 100%; 8 | } 9 | 10 | &-Dashboard { 11 | height: 100%; 12 | &-inner { 13 | background-color: var(--color-background); 14 | border-radius: var(--border-radius-4); 15 | border: 1px solid var(--color-border); 16 | color: var(--color-text-soft); 17 | width: auto !important; 18 | min-width: 200px; 19 | height: 100% !important; 20 | } 21 | 22 | &-AddFiles, 23 | &-dropFilesHereHint { 24 | border: 2px dashed var(--color-border); 25 | border-radius: var(--border-radius-3); 26 | margin: var(--m-s); 27 | color: inherit; 28 | font-size: var(--font-size-xxl); 29 | font-weight: 400; 30 | 31 | &-title { 32 | color: inherit; 33 | font-size: inherit; 34 | } 35 | 36 | &-list:has(span[role="presentation"]:empty) { 37 | display: none; 38 | } 39 | } 40 | 41 | &-browse { 42 | color: var(--importer-link); 43 | } 44 | 45 | &-dropFilesHereHint { 46 | padding: 0; 47 | background-image: none; 48 | inset: 0; 49 | border-color: var(--color-text-soft); 50 | color: var(--color-text); 51 | border-width: 3px; 52 | } 53 | 54 | &-Item { 55 | max-width: inherit; 56 | &-previewInnerWrap { 57 | background-color: var(--color-background) !important; 58 | border-radius: var(--border-radius-1); 59 | height: 100% !important; 60 | &::after { 61 | background-color: transparent; 62 | } 63 | } 64 | &-status { 65 | color: inherit; 66 | } 67 | &-action { 68 | &--remove { 69 | width: auto; 70 | height: auto; 71 | 72 | svg { 73 | width: var(--m); 74 | height: var(--m); 75 | aspect-ratio: 1; 76 | 77 | stroke: none; 78 | 79 | path { 80 | &:nth-of-type(1) { 81 | fill: var(--color-text-soft); 82 | } 83 | &:nth-of-type(2) { 84 | fill: var(--color-background); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | &-progressIndicator { 92 | width: auto; 93 | height: auto; 94 | 95 | svg { 96 | width: var(--m); 97 | height: var(--m); 98 | aspect-ratio: 1; 99 | 100 | circle { 101 | fill: var(--color-green-ui); 102 | } 103 | polygon { 104 | fill: var(--color-text-on-primary); 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | &-DashboardContent { 112 | &-bar { 113 | background-color: transparent; 114 | border-bottom-color: var(--color-border); 115 | height: auto; 116 | padding: var(--m-s); 117 | display: flex; 118 | justify-content: space-between; 119 | width: auto; 120 | 121 | & > * { 122 | flex: 0 1 auto; 123 | 124 | &:empty { 125 | display: none; 126 | } 127 | } 128 | } 129 | &-title { 130 | all: unset; 131 | } 132 | &-addMore, 133 | &-back, 134 | &-save { 135 | color: var(--color-text); 136 | padding: 0; 137 | font-size: inherit; 138 | display: inline; 139 | margin: 0; 140 | 141 | &:focus { 142 | background-color: transparent; 143 | outline: 2px solid var(--color-text); 144 | outline-offset: var(--m-xxxs); 145 | } 146 | } 147 | } 148 | 149 | &-StatusBar { 150 | border-top-color: transparent; 151 | height: auto; 152 | background-color: transparent; 153 | padding: var(--m-s); 154 | 155 | &[aria-hidden="true"] { 156 | display: none; 157 | } 158 | 159 | &-progress { 160 | left: 0; 161 | top: 0; 162 | background-color: var(--color-green-ui); 163 | } 164 | 165 | &:before { 166 | background-color: var(--color-border); 167 | height: 0; 168 | } 169 | 170 | &-content { 171 | color: var(--color-text-soft); 172 | font-size: inherit; 173 | padding-left: 0; 174 | } 175 | 176 | &-actions { 177 | padding: var(--m-s); 178 | display: flex; 179 | justify-content: flex-end; 180 | background-color: transparent; 181 | right: 0; 182 | 183 | &:empty { 184 | display: none; 185 | } 186 | } 187 | 188 | &-actionBtn { 189 | color: var(--color-text); 190 | border-radius: var(--border-radius-1); 191 | padding: var(--m-xxxs) var(--m-xxs); 192 | border: 1px solid var(--color-border); 193 | background-color: transparent; 194 | margin-right: var(--m-xs); 195 | font-size: inherit; 196 | height: auto; 197 | display: none; 198 | gap: var(--m-xxxs); 199 | 200 | svg { 201 | position: static; 202 | } 203 | 204 | &:not([disabled]):hover { 205 | border-color: var(--color-border-hover); 206 | } 207 | 208 | &--upload { 209 | padding: 0.5px var(--m-s); 210 | border-radius: var(--border-radius-2); 211 | font-size: var(--font-size-xs); 212 | font-weight: 400; 213 | background-color: var(--color-primary); 214 | color: var(--color-text-on-primary); 215 | 216 | &:not([disabled]):not(.disableHover):hover, 217 | &:not([disabled]):not(.disableHover):active { 218 | background-color: var(--color-primary-hover); 219 | color: var(--color-text-on-primary); 220 | } 221 | 222 | &:is([disabled]) { 223 | background-color: var(--color-primary-disabled); 224 | color: var(--color-text-on-primary-disabled); 225 | } 226 | } 227 | } 228 | 229 | &-statusPrimary { 230 | line-height: inherit; 231 | } 232 | 233 | &-statusIndicator { 234 | color: var(--color-green-ui); 235 | } 236 | 237 | &.is-error { 238 | .uppy-StatusBar-statusIndicator { 239 | color: var(--color-red); 240 | } 241 | .uppy-StatusBar-statusPrimary { 242 | color: var(--color-red); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/importer/components/UploaderWrapper/types/index.ts: -------------------------------------------------------------------------------- 1 | import { UploaderProps } from "../../../features/uploader/types"; 2 | 3 | export type UploaderWrapperProps = Omit & {}; 4 | -------------------------------------------------------------------------------- /src/importer/features/complete/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { Button } from "@chakra-ui/button"; 3 | import Box from "../../components/Box"; 4 | import { CompleteProps } from "./types"; 5 | import style from "./style/Complete.module.scss"; 6 | import { PiArrowCounterClockwise, PiCheckBold } from "react-icons/pi"; 7 | 8 | export default function Complete({ reload, close, isModal }: CompleteProps) { 9 | const { t } = useTranslation(); 10 | return ( 11 | 12 | <> 13 | 14 | 15 | 16 |
{t("Import Successful")}
17 |
18 | 21 | {isModal && ( 22 | 25 | )} 26 |
27 | 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/importer/features/complete/style/Complete.module.scss: -------------------------------------------------------------------------------- 1 | .content.content { 2 | max-width: 1000px; 3 | padding-top: var(--m); 4 | height: 100%; 5 | flex: 1 0 100px; 6 | box-shadow: none; 7 | background-color: transparent; 8 | align-self: center; 9 | 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | font-size: var(--font-size-xl); 14 | flex-direction: column; 15 | gap: var(--m); 16 | text-align: center; 17 | position: relative; 18 | 19 | .icon { 20 | width: 64px; 21 | height: 64px; 22 | isolation: isolate; 23 | position: relative; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | 28 | &::before { 29 | content: ""; 30 | position: absolute; 31 | inset: 0; 32 | border-radius: 50%; 33 | background-color: var(--color-green-ui); 34 | z-index: -1; 35 | } 36 | 37 | svg { 38 | width: 38%; 39 | height: 38%; 40 | object-fit: contain; 41 | color: var(--color-text-on-primary); 42 | } 43 | } 44 | 45 | .actions { 46 | display: flex; 47 | gap: var(--m-l); 48 | align-items: center; 49 | justify-content: center; 50 | margin-top: var(--m-xxl); 51 | 52 | & > * { 53 | flex: 1 0 190px; 54 | } 55 | 56 | button { 57 | width: 50%; 58 | } 59 | } 60 | } 61 | 62 | .spinner { 63 | border: 1px solid var(--color-border); 64 | margin-top: var(--m); 65 | padding: var(--m); 66 | border-radius: var(--border-radius-1); 67 | } 68 | -------------------------------------------------------------------------------- /src/importer/features/complete/types/index.ts: -------------------------------------------------------------------------------- 1 | export type CompleteProps = { 2 | reload: () => void; 3 | close: () => void; 4 | isModal: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /src/importer/features/main/hooks/useMutableLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export default function useMutableLocalStorage(key: string, initialValue: any) { 4 | // State to store our value 5 | // Pass initial state function to useState so logic is only executed once 6 | const getLocalStorage = () => { 7 | if (typeof window === "undefined") { 8 | return initialValue; 9 | } 10 | try { 11 | // Get from local storage by key 12 | const item = window.localStorage.getItem(key); 13 | // Parse stored json or if none return initialValue 14 | return item ? JSON.parse(item) : initialValue; 15 | } catch (error) { 16 | // If error also return initialValue 17 | console.log(error); 18 | return initialValue; 19 | } 20 | }; 21 | const [storedValue, setStoredValue] = useState(getLocalStorage()); 22 | 23 | useEffect(() => { 24 | setStoredValue(getLocalStorage()); 25 | }, [key]); 26 | 27 | // Return a wrapped version of useState's setter function that ... 28 | // ... persists the new value to localStorage. 29 | const setValue = (value: any) => { 30 | try { 31 | // Allow value to be a function so we have same API as useState 32 | const valueToStore = value instanceof Function ? value(storedValue) : value; 33 | // Save state 34 | setStoredValue(valueToStore); 35 | // Save to local storage 36 | if (typeof window !== "undefined") { 37 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 38 | } 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }; 43 | 44 | return [storedValue, setValue]; 45 | } 46 | -------------------------------------------------------------------------------- /src/importer/features/main/hooks/useStepNavigation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import useStepper from "../../../components/Stepper/hooks/useStepper"; 4 | import { Steps } from "../types"; 5 | import useMutableLocalStorage from "./useMutableLocalStorage"; 6 | 7 | export const StepEnum = { 8 | Upload: 0, 9 | RowSelection: 1, 10 | MapColumns: 2, 11 | Complete: 3, 12 | }; 13 | 14 | const calculateNextStep = (nextStep: number, skipHeader: boolean) => { 15 | if (skipHeader) { 16 | switch (nextStep) { 17 | case StepEnum.Upload: 18 | case StepEnum.RowSelection: 19 | return StepEnum.MapColumns; 20 | case StepEnum.MapColumns: 21 | return StepEnum.Complete; 22 | default: 23 | return nextStep; 24 | } 25 | } 26 | return nextStep; 27 | }; 28 | 29 | const getStepConfig = (skipHeader: boolean) => { 30 | return [ 31 | { label: "Upload", id: Steps.Upload }, 32 | { label: "Select Header", id: Steps.RowSelection, disabled: skipHeader }, 33 | { label: "Map Columns", id: Steps.MapColumns }, 34 | ]; 35 | }; 36 | 37 | function useStepNavigation(initialStep: number, skipHeader: boolean) { 38 | const [t] = useTranslation(); 39 | const translatedSteps = getStepConfig(skipHeader).map((step) => ({ 40 | ...step, 41 | label: t(step.label), 42 | })); 43 | const stepper = useStepper(translatedSteps, StepEnum.Upload, skipHeader); 44 | const [storageStep, setStorageStep] = useMutableLocalStorage(`tf_steps`, ""); 45 | const [currentStep, setCurrentStep] = useState(initialStep); 46 | 47 | const goBack = (backStep = 0) => { 48 | backStep = backStep || currentStep - 1 || 0; 49 | setStep(backStep); 50 | }; 51 | 52 | const goNext = (nextStep = 0) => { 53 | nextStep = nextStep || currentStep + 1 || 0; 54 | const calculatedStep = calculateNextStep(nextStep, skipHeader); 55 | setStep(calculatedStep); 56 | }; 57 | 58 | const setStep = (newStep: number) => { 59 | setCurrentStep(newStep); 60 | setStorageStep(newStep); 61 | stepper.setCurrent(newStep); 62 | }; 63 | 64 | useEffect(() => { 65 | stepper.setCurrent(storageStep || 0); 66 | setCurrentStep(storageStep || 0); 67 | }, [storageStep]); 68 | 69 | return { 70 | currentStep: storageStep || currentStep, 71 | setStep, 72 | goBack, 73 | goNext, 74 | stepper, 75 | stepId: stepper?.step?.id, 76 | setStorageStep, 77 | }; 78 | } 79 | 80 | export default useStepNavigation; 81 | -------------------------------------------------------------------------------- /src/importer/features/main/index.tsx: -------------------------------------------------------------------------------- 1 | import Papa from "papaparse"; 2 | import { useEffect, useState } from "react"; 3 | import * as XLSX from "xlsx"; 4 | import { IconButton } from "@chakra-ui/button"; 5 | import Errors from "../../components/Errors"; 6 | import Stepper from "../../components/Stepper"; 7 | import { CSVImporterProps } from "../../../types"; 8 | import useCustomStyles from "../../hooks/useCustomStyles"; 9 | import { Template } from "../../types"; 10 | import { convertRawTemplate } from "../../utils/template"; 11 | import { parseObjectOrStringJSON } from "../../utils/utils"; 12 | import { TemplateColumnMapping } from "../map-columns/types"; 13 | import useStepNavigation, { StepEnum } from "./hooks/useStepNavigation"; 14 | import { FileData, FileRow } from "./types"; 15 | import style from "./style/Main.module.scss"; 16 | import Complete from "../complete"; 17 | import MapColumns from "../map-columns"; 18 | import RowSelection from "../row-selection"; 19 | import Uploader from "../uploader"; 20 | import { PiX } from "react-icons/pi"; 21 | import { useTranslation } from "react-i18next"; 22 | 23 | export default function Main(props: CSVImporterProps) { 24 | const { 25 | isModal = true, 26 | modalOnCloseTriggered = () => null, 27 | template, 28 | onComplete, 29 | customStyles, 30 | showDownloadTemplateButton, 31 | skipHeaderRowSelection, 32 | } = props; 33 | const skipHeader = skipHeaderRowSelection ?? false; 34 | 35 | const { t } = useTranslation(); 36 | 37 | // Apply custom styles 38 | useCustomStyles(parseObjectOrStringJSON("customStyles", customStyles)); 39 | 40 | // Stepper handler 41 | const { currentStep, setStep, goNext, goBack, stepper, setStorageStep } = useStepNavigation(StepEnum.Upload, skipHeader); 42 | 43 | // Error handling 44 | const [initializationError, setInitializationError] = useState(null); 45 | const [dataError, setDataError] = useState(null); 46 | 47 | // File data 48 | const emptyData = { 49 | fileName: "", 50 | rows: [], 51 | sheetList: [], 52 | errors: [], 53 | }; 54 | const [data, setData] = useState(emptyData); 55 | 56 | // Header row selection state 57 | const [selectedHeaderRow, setSelectedHeaderRow] = useState(0); 58 | 59 | // Map of upload column index -> TemplateColumnMapping 60 | const [columnMapping, setColumnMapping] = useState<{ [index: number]: TemplateColumnMapping }>({}); 61 | 62 | // Used in the final step to show a loading indicator while the data is submitting 63 | const [isSubmitting, setIsSubmitting] = useState(false); 64 | 65 | const [parsedTemplate, setParsedTemplate] = useState