├── .env ├── .gitignore ├── README.md ├── cosmos.config.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html ├── locales │ ├── cn │ │ └── translation.json │ ├── en │ │ └── translation.json │ └── vi │ │ └── translation.json ├── logo192.png ├── manifest.json ├── robots.txt └── static │ └── image │ ├── avatar.png │ ├── china.svg │ ├── co.png │ ├── coming.jpg │ ├── f.svg │ ├── file.png │ ├── huyen.JPG │ ├── ngoc.JPG │ ├── phuc.JPG │ ├── snapshot.png │ ├── son.jpg │ ├── thay.png │ ├── united-kingdom.svg │ ├── vietnam.svg │ └── vn.jpg ├── src ├── App.test.tsx ├── __fixtures__ │ ├── auth │ │ ├── AuthProvider.tsx │ │ └── index.tsx │ ├── chart │ │ ├── areaChartInput │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ ├── barChartInput │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ ├── chartDrawer │ │ │ ├── area.tsx │ │ │ ├── bar.tsx │ │ │ ├── index.ts │ │ │ ├── line.tsx │ │ │ ├── pie.tsx │ │ │ └── scatter.tsx │ │ ├── dummyInput │ │ │ └── dummy.tsx │ │ ├── index.tsx │ │ ├── lineChartInput │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ ├── pieChartInput │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ ├── scatterChartInput │ │ │ ├── index.tsx │ │ │ └── reducer.ts │ │ └── standardChart │ │ │ ├── CommonStandard.tsx │ │ │ ├── area.tsx │ │ │ ├── bar.tsx │ │ │ ├── line.tsx │ │ │ ├── pie.tsx │ │ │ └── scatter.tsx │ ├── chartCard │ │ └── index.tsx │ ├── colorSetter │ │ └── index.tsx │ ├── delayinput │ │ └── index.tsx │ ├── history │ │ └── index.tsx │ ├── icons │ │ └── index.tsx │ ├── index.tsx │ ├── initState.ts │ ├── layout │ │ ├── index.tsx │ │ └── routeConfig.tsx │ ├── loading │ │ └── Gears.tsx │ ├── logo │ │ └── index.tsx │ ├── menu │ │ └── index.tsx │ ├── navigator │ │ ├── index.tsx │ │ └── style.css │ ├── pages │ │ ├── About.tsx │ │ ├── Chart.tsx │ │ ├── Home.tsx │ │ ├── QA.tsx │ │ ├── User.tsx │ │ ├── Weather.tsx │ │ └── index.ts │ ├── userInfo │ │ ├── croppie.css │ │ ├── editor.tsx │ │ └── index.tsx │ └── utils │ │ └── index.ts ├── constants │ └── index.ts ├── graphql │ ├── mutations.ts │ └── queries.ts ├── i18n.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts └── tailwind │ ├── in.css │ └── out.css ├── tailwind.config.js └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | HTTPS=true 2 | REACT_APP_DEVELOPMENT_API_URL=https://localhost:4000 3 | REACT_APP_PRODUCTION_API_URL=something 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/leminhson2398/graph-maker-front-end.svg?branch=master)](https://travis-ci.org/leminhson2398/graph-maker-front-end) 3 | 4 |

chart demo

5 | 6 | ### Features 7 | 1) Multi languges support (English, Vietnamese, Chinese) 8 | 2) Draw charts quickly as user fill in the chart data form 9 | 3) Save chart data in database or as png files so users can view them later 10 | 4) 5 chart popular chart types supported 11 |
    12 |
  1. area
  2. 13 |
  3. scatter
  4. 14 |
  5. line
  6. 15 |
  7. bar
  8. 16 |
  9. pie (doughnut)
  10. 17 |
18 | 19 | ### Design file 20 | Figma link 21 | 22 | ### `npm start` 23 | 24 | Runs the app in the development mode.
25 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 26 | 27 | The page will reload if you make edits.
28 | You will also see any lint errors in the console. 29 | 30 | ### `npm run build` 31 | 32 | Builds the app for production to the `build` folder.
33 | It correctly bundles React in production mode and optimizes the build for the best performance. 34 | 35 | The build is minified and the filenames include the hashes.
36 | Your app is ready to be deployed! 37 | 38 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 39 | 40 | ### contribution guide 41 | Please create a pull request and i will review it as soon as possible 42 | Thank you! 43 | -------------------------------------------------------------------------------- /cosmos.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "watchDirs": [ 3 | "src" 4 | ], 5 | "webpack": { 6 | "configPath": "react-scripts/config/webpack.config" 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.3.6", 7 | "@material-ui/core": "^4.11.2", 8 | "@material-ui/icons": "^4.11.2", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.5.0", 11 | "@testing-library/user-event": "^7.2.1", 12 | "@types/apollo-upload-client": "^14.1.0", 13 | "@types/chart.js": "^2.9.29", 14 | "@types/croppie": "^2.5.5", 15 | "@types/jest": "^24.9.1", 16 | "@types/node": "^12.19.9", 17 | "@types/react": "^16.14.2", 18 | "@types/react-color": "^3.0.4", 19 | "@types/react-dom": "^16.9.10", 20 | "@types/react-helmet": "^6.1.0", 21 | "@types/react-router-dom": "^5.1.6", 22 | "@types/socket.io-client": "^1.4.34", 23 | "apollo-upload-client": "^14.1.3", 24 | "chart.js": "^2.9.4", 25 | "croppie": "^2.6.5", 26 | "dayjs": "^1.9.7", 27 | "graphql": "^15.4.0", 28 | "html2canvas": "^1.0.0-rc.7", 29 | "i18next": "^19.8.4", 30 | "i18next-browser-languagedetector": "^6.0.1", 31 | "i18next-http-backend": "^1.0.21", 32 | "react": "^16.14.0", 33 | "react-color": "^2.19.3", 34 | "react-dom": "^16.14.0", 35 | "react-helmet": "^6.1.0", 36 | "react-i18next": "^11.8.4", 37 | "react-router-dom": "^5.2.0", 38 | "react-scripts": "^3.4.4", 39 | "react-transition-group": "^4.4.1", 40 | "rxjs": "^6.6.3", 41 | "simplebar-react": "^2.3.0", 42 | "socket.io-client": "^2.3.1", 43 | "typescript": "^3.9.7" 44 | }, 45 | "scripts": { 46 | "start": "npm run watch:css && react-scripts start", 47 | "build": "GENERATE_SOURCEMAP=false npm run build:css && react-scripts build", 48 | "test": "react-scripts test", 49 | "eject": "react-scripts eject", 50 | "build:css": "NODE_ENV=production postcss src/tailwind/in.css -o src/tailwind/out.css", 51 | "watch:css": "postcss src/tailwind/in.css -o src/tailwind/out.css", 52 | "exp": "npm run watch:css && npx react-cosmos" 53 | }, 54 | "eslintConfig": { 55 | "extends": "react-app" 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | }, 69 | "devDependencies": { 70 | "autoprefixer": "^9.8.6", 71 | "postcss-cli": "^7.1.2", 72 | "react-is": "^16.13.1", 73 | "tailwindcss": "^1.9.6" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | // ... 4 | require('tailwindcss'), 5 | require('autoprefixer'), 6 | // ... 7 | ] 8 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | Schart - Charts for students 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /public/locales/cn/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "weather": "天气", 3 | "chart": "图表", 4 | "save": "保存", 5 | "cancel": "取消", 6 | "about": "关于我们", 7 | "qa": "问题与答案", 8 | "signout": "退出", 9 | "myProfile": "我的简历", 10 | "login": "登录", 11 | "setColor": "设置颜色", 12 | "delete": "删除", 13 | "update": "更新", 14 | "chartType": { 15 | "bar": "柱形图", 16 | "line": "折线图", 17 | "pie": "圆盘形图表", 18 | "area": "区域图", 19 | "scatter": "散点图" 20 | }, 21 | "saveChart": { 22 | "name": "保存图表", 23 | "type": { 24 | "image": "图形", 25 | "cloud": "云" 26 | } 27 | }, 28 | "chartInput": { 29 | "headName": "输入值", 30 | "addItem": "添加事项", 31 | "removeItem": "删除事项", 32 | "title": "图表名称", 33 | "xLabel": "Ox 标签", 34 | "yLabel": "Oy 标签", 35 | "dataOnOx": "Ox 上的数据", 36 | "dataOnOy": "Oy 上的数据", 37 | "placeholder": { 38 | "enterValue": "输入数值" 39 | }, 40 | "bar": { 41 | "block": "块", 42 | "placeholder": { 43 | "enterName": "输入表头", 44 | "enterBlockName": "输入模块名" 45 | }, 46 | "barName": "表名" 47 | }, 48 | "line": { 49 | "addLine": "添加一行", 50 | "deleteLine": "删除一行", 51 | "placeholder": { 52 | "lineName": "行名" 53 | } 54 | }, 55 | "pie": { 56 | "pie": "圆盘形图表", 57 | "pieName": "餡餅名稱", 58 | "slice": "一部分", 59 | "sliceName": "一部分名称", 60 | "placeholder": { 61 | "enterPieName": "輸入餅名", 62 | "enterSliceName": "輸入切片名稱" 63 | }, 64 | "addPie": "添加圆盘形图表", 65 | "addSlice": "添加一部分", 66 | "removePie": "去除一部分", 67 | "removeSlice": "删除一部分", 68 | "value": "数值" 69 | }, 70 | "area": { 71 | "addArea": "添加区域", 72 | "removeArea": "删除区域", 73 | "placeholder": { 74 | "areaName": "地区名称" 75 | } 76 | } 77 | }, 78 | "collection": { 79 | "collection": "收集", 80 | "filter": "过滤", 81 | "filterDate": { 82 | "olderFirst": "旧数据优先", 83 | "latestFirst": "新数据优先", 84 | "placeholder": "生成日期" 85 | }, 86 | "filterChartType": { 87 | "placeholder": "图表类型" 88 | } 89 | }, 90 | "aboutUser": { 91 | "about": "数据", 92 | "name": "名称", 93 | "email": "电子邮件", 94 | "thoughtAboutGeo": "你对地理的热爱", 95 | "changeAva": "更改头像", 96 | "chooseNewAva": "选择新图片" 97 | } 98 | } -------------------------------------------------------------------------------- /public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "weather": "Weather", 3 | "chart": "Chart", 4 | "save": "save", 5 | "cancel": "cancel", 6 | "about": "About us", 7 | "qa": "Q & A", 8 | "signout": "Sign out", 9 | "myProfile": "my profile", 10 | "login": "login", 11 | "setColor": "Set color", 12 | "delete": "Delete", 13 | "update": "Update", 14 | "chartType": { 15 | "bar": "Bar chart", 16 | "line": "Line chart", 17 | "pie": "Pie chart", 18 | "area": "Area chart", 19 | "scatter": "Scatter chart" 20 | }, 21 | "saveChart": { 22 | "name": "Save chart", 23 | "type": { 24 | "image": "image", 25 | "cloud": "cloud" 26 | } 27 | }, 28 | "chartInput": { 29 | "headName": "Input data", 30 | "addItem": "Add item", 31 | "removeItem": "remove item", 32 | "title": "Chart title", 33 | "xLabel": "Ox Label", 34 | "yLabel": "Oy Label", 35 | "dataOnOx": "Data on Ox", 36 | "dataOnOy": "Data on Oy", 37 | "placeholder": { 38 | "enterValue": "Enter value" 39 | }, 40 | "bar": { 41 | "block": "block", 42 | "placeholder": { 43 | "enterName": "Enter bar name", 44 | "enterBlockName": "Enter block name" 45 | }, 46 | "barName": "bar name" 47 | }, 48 | "line": { 49 | "addLine": "Add line", 50 | "deleteLine": "Delete line", 51 | "placeholder": { 52 | "lineName": "Line name" 53 | } 54 | }, 55 | "pie": { 56 | "pie": "Pie", 57 | "pieName": "pie name", 58 | "slice": "Slice", 59 | "sliceName": "slice name", 60 | "placeholder": { 61 | "enterPieName": "Enter pie name", 62 | "enterSliceName": "Enter slice name" 63 | }, 64 | "addPie": "Add pie", 65 | "addSlice": "Add slice", 66 | "removePie": "Remove pie", 67 | "removeSlice": "Delete slice", 68 | "value": "value" 69 | }, 70 | "area": { 71 | "addArea": "Add area", 72 | "removeArea": "Delete area", 73 | "placeholder": { 74 | "areaName": "Area name" 75 | } 76 | } 77 | }, 78 | "collection": { 79 | "collection": "Collection", 80 | "filter": "Filter", 81 | "filterDate": { 82 | "olderFirst": "older first", 83 | "latestFirst": "latest first", 84 | "placeholder": "create date" 85 | }, 86 | "filterChartType": { 87 | "placeholder": "chart type" 88 | } 89 | }, 90 | "aboutUser": { 91 | "about": "Information", 92 | "name": "name", 93 | "email": "email", 94 | "thoughtAboutGeo": "Your love for gepgraphy ?", 95 | "changeAva": "Change avatar", 96 | "chooseNewAva": "Choose new image" 97 | } 98 | } -------------------------------------------------------------------------------- /public/locales/vi/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "weather": "Thời tiết", 3 | "chart": "Biểu đồ ", 4 | "save": "Lưu", 5 | "cancel": "huỷ", 6 | "about": "Về chúng tôi", 7 | "qa": "Hỏi đáp", 8 | "signout": "Đăng xuất", 9 | "myProfile": "Trang cá nhân", 10 | "login": "Đăng nhập", 11 | "setColor": "Thay màu", 12 | "delete": "Xoá", 13 | "update": "Cập nhật", 14 | "chartType": { 15 | "bar": "Biểu đồ cột", 16 | "line": "Biểu đồ đường", 17 | "pie": "Biểu đồ quạt", 18 | "area": "Biểu đồ miền", 19 | "scatter": "Biểu đồ phân tán" 20 | }, 21 | "saveChart": { 22 | "name": "Lưu biểu đồ ", 23 | "type": { 24 | "image": "hình ảnh", 25 | "cloud": "đám mây" 26 | } 27 | }, 28 | "chartInput": { 29 | "headName": "Dữ liệu vào", 30 | "addItem": "Thêm mục", 31 | "removeItem": "Xoá mục", 32 | "title": "Tên biểu đồ ", 33 | "xLabel": "Tên trục Ox", 34 | "yLabel": "Tên trục Oy", 35 | "dataOnOx": "Dữ liệu trên Ox", 36 | "dataOnOy": "Dữ liệu trên Oy", 37 | "placeholder": { 38 | "enterValue": "Nhập giá trị" 39 | }, 40 | "bar": { 41 | "block": "Khối", 42 | "placeholder": { 43 | "enterName": "Nhập tên cột", 44 | "enterBlockName": "Nhập tên khối" 45 | }, 46 | "barName": "tên cột" 47 | }, 48 | "line": { 49 | "addLine": "Thêm đường", 50 | "deleteLine": "Xoá đường", 51 | "placeholder": { 52 | "lineName": "Tên đường" 53 | } 54 | }, 55 | "pie": { 56 | "pie": "Bánh", 57 | "pieName": "tên bánh", 58 | "slice": "Lát", 59 | "sliceName": "tên lát", 60 | "placeholder": { 61 | "enterPieName": "Nhập tên bánh", 62 | "enterSliceName": "Nhập tên lát" 63 | }, 64 | "addPie": "Thêm bánh", 65 | "addSlice": "Thêm lát", 66 | "removePie": "Xoá bánh", 67 | "removeSlice": "Xoá lát", 68 | "value": "giá trị" 69 | }, 70 | "area": { 71 | "addArea": "Thêm miền", 72 | "removeArea": "Xoá miền", 73 | "placeholder": { 74 | "areaName": "Tên miền" 75 | } 76 | } 77 | }, 78 | "collection": { 79 | "collection": "Bộ sưu tập", 80 | "filter": "Bộ lọc", 81 | "filterDate": { 82 | "olderFirst": "cũ trước", 83 | "latestFirst": "mới trước", 84 | "placeholder": "ngày tạo" 85 | }, 86 | "filterChartType": { 87 | "placeholder": "kiểu biểu đồ" 88 | } 89 | }, 90 | "aboutUser": { 91 | "about": "Thông tin", 92 | "name": "tên", 93 | "email": "email", 94 | "thoughtAboutGeo": "Tình yêu của bạn dành cho địa lý ?", 95 | "changeAva": "Thay ảnh đại diện", 96 | "chooseNewAva": "Chọn ảnh mới" 97 | } 98 | } -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/logo192.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/static/image/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/avatar.png -------------------------------------------------------------------------------- /public/static/image/china.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 12 | 15 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /public/static/image/co.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/co.png -------------------------------------------------------------------------------- /public/static/image/coming.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/coming.jpg -------------------------------------------------------------------------------- /public/static/image/f.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/static/image/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/file.png -------------------------------------------------------------------------------- /public/static/image/huyen.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/huyen.JPG -------------------------------------------------------------------------------- /public/static/image/ngoc.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/ngoc.JPG -------------------------------------------------------------------------------- /public/static/image/phuc.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/phuc.JPG -------------------------------------------------------------------------------- /public/static/image/snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/snapshot.png -------------------------------------------------------------------------------- /public/static/image/son.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/son.jpg -------------------------------------------------------------------------------- /public/static/image/thay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/thay.png -------------------------------------------------------------------------------- /public/static/image/united-kingdom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 13 | 15 | 17 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /public/static/image/vietnam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/static/image/vn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamleson98/graph-maker-front-end/c97bb26d9266acfc532cedc5244bb49a3b35fb40/public/static/image/vn.jpg -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './__fixtures__'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__fixtures__/auth/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useMemo, useRef, useState } from "react" 2 | import { interval, Subscription } from "rxjs" 3 | 4 | 5 | interface AuthPrvdProps { 6 | children: JSX.Element; 7 | provider: string; 8 | socket: any; 9 | } 10 | 11 | const { NODE_ENV, REACT_APP_DEVELOPMENT_API_URL, REACT_APP_PRODUCTION_API_URL } = process.env 12 | 13 | function AuthPrvd({ children, provider, socket }: AuthPrvdProps) { 14 | 15 | const [state, setState] = useState({ 16 | user: null, 17 | }) 18 | const { user } = state 19 | 20 | // ref 21 | const popupRef = useRef() 22 | const $subRef = useRef() 23 | 24 | const AUTH_URL = useMemo(() => { 25 | return NODE_ENV === "development" ? 26 | REACT_APP_DEVELOPMENT_API_URL as string : 27 | REACT_APP_PRODUCTION_API_URL as string 28 | }, []) 29 | 30 | const performAuthen = () => { 31 | $subRef.current = interval(500).subscribe(() => { 32 | if (!!user && !!popupRef.current && !popupRef.current.closed) { // authenticated successfull, popup still not closed yet 33 | popupRef.current.close() 34 | } 35 | }) 36 | openPopup() 37 | } 38 | 39 | const openPopup = () => { 40 | const width = 600, height = 600 41 | const left = (window.innerWidth / 2) - (width / 2) 42 | const top = (window.innerHeight / 2) - (height / 2) 43 | const url = `${AUTH_URL}/${provider}?socketId=${socket.id}` 44 | 45 | popupRef.current = window.open( 46 | url, 47 | "", 48 | `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${top}, left=${left}` 49 | ) as Window 50 | } 51 | 52 | useEffect(() => { 53 | socket.on(provider, (user: any) => { 54 | // close popup first 55 | console.log(user) 56 | popupRef.current?.close() 57 | setState({ 58 | ...state, 59 | user 60 | }) 61 | }) 62 | 63 | return () => { 64 | $subRef.current?.unsubscribe() 65 | } 66 | }, [provider, state, socket]) 67 | 68 | return ( 69 |
72 | {children} 73 |
74 | ) 75 | } 76 | 77 | export default memo(AuthPrvd); 78 | -------------------------------------------------------------------------------- /src/__fixtures__/auth/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo, useState } from "react" 2 | import DelayInput from "../delayinput" 3 | import { Button, FormHelperText, SvgIcon, Tooltip } from "@material-ui/core" 4 | import { Email, Visibility, VisibilityOff } from "@material-ui/icons" 5 | import { Helmet } from "react-helmet" 6 | import io from "socket.io-client" 7 | import AuthPrvd from "./AuthProvider" 8 | 9 | 10 | const Google = (props: any) => ( 11 | 12 | 13 | 14 | ) 15 | 16 | const Facebook = (props: any) => ( 17 | 18 | 19 | 20 | ) 21 | 22 | const Twitter = (props: any) => ( 23 | 24 | 25 | 26 | ) 27 | 28 | // const staticUrl = "/static" 29 | 30 | function Auth() { 31 | 32 | const { REACT_APP_DEVELOPMENT_API_URL, REACT_APP_PRODUCTION_API_URL, NODE_ENV } = process.env 33 | 34 | // state 35 | const [state, setState] = useState({ 36 | email: "", 37 | password: "", 38 | passwordVisible: false, 39 | 40 | emailError: null, 41 | passwordErr: null 42 | }) 43 | const { email, password, passwordVisible, emailError, passwordErr } = state 44 | 45 | // memoized values: 46 | const socialButtons = useMemo(() => { 47 | return [ 48 | { name: "Facebook", icon: Facebook, bgClass: "bg-blue-700", bgHoverClass: "hover:bg-blue-600" }, 49 | { name: "Google", icon: Google, bgClass: "bg-red-600", bgHoverClass: "hover:bg-red-500" }, 50 | { name: "Twitter", icon: Twitter, bgClass: "bg-blue-500", bgHoverClass: "hover:bg-blue-400" }, 51 | ] 52 | }, []) 53 | 54 | const changeHandler = (type: "password" | "email") => (value: string) => { 55 | 56 | } 57 | 58 | return ( 59 |
65 | 66 | 67 |
74 |
75 | 84 | 85 |
86 | )} 87 | /> 88 |
89 | {!!emailError && {emailError}} 90 | { 100 | setState({ 101 | ...state, 102 | passwordVisible: !passwordVisible 103 | }) 104 | }} 105 | > 106 | {passwordVisible ? : } 107 |
108 | )} 109 | /> 110 | {!!passwordErr && {passwordErr}} 111 |
112 | 121 |
122 |
123 |
124 | OR 125 |
126 |
127 | {socialButtons.map((button, idx) => ( 128 | 129 |
130 | 134 | 135 | {button.name} 136 |
137 | )} 138 | provider={button.name.toLowerCase()} 139 | socket={io(NODE_ENV === "development" ? REACT_APP_DEVELOPMENT_API_URL as string : REACT_APP_PRODUCTION_API_URL as string)} 140 | /> 141 |
142 | 143 | ))} 144 | 145 | 146 | 147 | 148 | ) 149 | } 150 | 151 | export default memo(Auth) 152 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/areaChartInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useReducer } from "react" 2 | import { useTranslation } from "react-i18next" 3 | import { localState } from "../.." 4 | import { AreaChartAction, AreaChartState, areaChartReducer, typeChange, MAX_AREAS } from "./reducer" 5 | import { Add, Remove } from "@material-ui/icons" 6 | import Tooltip from "@material-ui/core/Tooltip" 7 | import Button from "@material-ui/core/Button" 8 | import ColorSetter from "../../colorSetter" 9 | import DelayInput from "../../delayinput" 10 | import { noAnyError } from "../../utils" 11 | 12 | 13 | function AreaChartInput() { 14 | 15 | // trans 16 | const { t } = useTranslation() 17 | 18 | // component state 19 | const [state, dispatch] = useReducer>(areaChartReducer, localState().areaChartState) 20 | const { chartTitle, xLabel, xData, yLabel, yData } = state 21 | 22 | useEffect(() => { 23 | if (noAnyError(state.yData.map(area => area.error))) { 24 | localState({ 25 | ...localState(), 26 | areaChartState: state, 27 | chartDrawMutexReleased: true 28 | }) 29 | } 30 | }, [state]) 31 | 32 | return ( 33 |
34 | {/* chart title */} 35 |
36 | 37 | dispatch({ 44 | type: typeChange.chartTitle, 45 | value 46 | })} 47 | /> 48 |
49 | 50 |
51 |
52 | {/* title on ox */} 53 |
54 | 55 | { 62 | dispatch({ 63 | type: typeChange.xLabel, 64 | value: value.trim() 65 | }) 66 | }} 67 | /> 68 |
69 | 70 | {/* x data */} 71 |
72 | 73 | {t("chartInput.dataOnOx")} 74 | 75 | {xData.map((item, idx) => ( 76 |
77 | {idx + 1} 78 | dispatch({ 85 | type: typeChange.xDataField, 86 | value: idx, 87 | options: { 88 | value 89 | } 90 | })} 91 | /> 92 | 96 |
{ 99 | dispatch({ 100 | type: typeChange[!idx ? "addXField" : "deleteXField"], 101 | value: idx 102 | }) 103 | }} 104 | > 105 | {!idx ? : } 106 |
107 |
108 |
109 | ))} 110 |
111 |
112 | 113 |
114 | {/* title on oy */} 115 |
116 |
117 | 118 | { 125 | dispatch({ 126 | type: typeChange.yLabel, 127 | value: value.trim() 128 | }) 129 | }} 130 | /> 131 |
132 |
133 | 134 | {/* Data on Oy */} 135 |
136 | 137 | {t("chartInput.dataOnOy")} 138 | 139 | {yData.map((item, index) => ( 140 |
144 | 145 | dispatch({ 150 | type: typeChange.areaName, 151 | value: index, 152 | options: { 153 | value 154 | } 155 | })} 156 | defaultValue={item.name} 157 | placeholder={`${t("chartInput.area.placeholder.areaName")}`} 158 | endAdornment={( 159 | { 161 | if (color !== item.color) { // only update state if colors don't match 162 | dispatch({ 163 | type: typeChange.color, 164 | value: index, 165 | options: { 166 | value: color 167 | } 168 | }) 169 | } 170 | }} 171 | defaultBg={item.color} 172 | /> 173 | )} 174 | /> 175 | 176 | {item.data.map((field, idx) => ( 177 |
181 | {idx + 1} 182 | dispatch({ 188 | type: typeChange.yDataField, 189 | value: index, 190 | options: { 191 | value, 192 | index: idx 193 | } 194 | })} 195 | defaultValue={field} 196 | /> 197 |
198 | ))} 199 | {!!item.error && {item.error}} 200 | {!!index && ( 201 |
202 | 218 |
219 | )} 220 |
221 | ))} 222 | 236 |
237 |
238 |
239 |
240 | ) 241 | } 242 | 243 | export default memo(AreaChartInput) 244 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/areaChartInput/reducer.ts: -------------------------------------------------------------------------------- 1 | import { isRealNumber, defaultFieldColor } from "../../../constants"; 2 | 3 | 4 | export enum typeChange { 5 | chartTitle, 6 | xLabel, 7 | yLabel, 8 | xDataField, 9 | yDataField, 10 | color, 11 | areaName, 12 | deleteArea, 13 | addArea, 14 | addXField, 15 | deleteXField 16 | } 17 | 18 | export interface AreaChartState { 19 | chartTitle: string; 20 | xLabel: string; 21 | xData: string[]; 22 | yLabel: string; 23 | yData: { 24 | color: string; 25 | name: string; 26 | data: string[]; 27 | error?: string; 28 | }[]; 29 | } 30 | 31 | export interface AreaChartAction { 32 | type: typeChange; 33 | value?: any; 34 | options?: { 35 | index?: number; 36 | value?: any; 37 | } 38 | } 39 | 40 | export const MAX_AREAS = 10, MAX_POINTS = 25 41 | 42 | type error = string | undefined 43 | function areaChartErrorChecker(data: string[]): error { 44 | return data.some(item => !isRealNumber.test(item)) ? "values need to be numbers" : undefined 45 | } 46 | 47 | export function areaChartReducer(state: AreaChartState, action: AreaChartAction): AreaChartState { 48 | const { type, value, options } = action 49 | const { xData, yData } = state 50 | 51 | switch (type) { 52 | case typeChange.chartTitle: 53 | return { 54 | ...state, 55 | chartTitle: value 56 | } 57 | case typeChange.xLabel: 58 | return { 59 | ...state, 60 | xLabel: value 61 | } 62 | case typeChange.yLabel: 63 | return { 64 | ...state, 65 | yLabel: value 66 | } 67 | case typeChange.color: 68 | // value is area index, options.value is color for that index 69 | return { 70 | ...state, 71 | yData: yData.map((item, idx) => { 72 | return (idx === value) ? 73 | { 74 | ...item, 75 | color: options?.value 76 | } : 77 | item 78 | }) 79 | } 80 | case typeChange.areaName: 81 | // value is index, options.value is value for that 82 | return { 83 | ...state, 84 | yData: yData.map((item, idx) => { 85 | return (idx === value) ? 86 | { 87 | ...item, 88 | name: options?.value 89 | } : 90 | item 91 | }) 92 | } 93 | case typeChange.xDataField: 94 | // value is index of x field, options.value is value for it 95 | return { 96 | ...state, 97 | xData: xData.map((item, idx) => { 98 | return idx === value ? options?.value : item 99 | }) 100 | } 101 | case typeChange.yDataField: 102 | // value is index of that area, options.index is index of y field, options.value is new value for that 103 | return { 104 | ...state, 105 | yData: yData.map((item, idx) => { 106 | if (idx === value) { 107 | const data = item.data.map((itm, idx) => { 108 | if (idx === options?.index) { 109 | return options.value 110 | } 111 | return itm 112 | }) 113 | return { 114 | ...item, 115 | data, 116 | error: areaChartErrorChecker(data) 117 | } 118 | } 119 | return item 120 | }) 121 | } 122 | case typeChange.addArea: 123 | return { 124 | ...state, 125 | yData: yData.concat({ 126 | name: "", 127 | data: [...(new Array(xData.length))].map(_ => ""), 128 | color: defaultFieldColor 129 | }) 130 | } 131 | case typeChange.addXField: 132 | if (xData.length < MAX_POINTS) { 133 | return { 134 | ...state, 135 | xData: xData.concat(""), 136 | yData: yData.map(item => { 137 | return { 138 | ...item, 139 | data: item.data.concat("") 140 | } 141 | }) 142 | } 143 | } 144 | return state 145 | case typeChange.deleteXField: 146 | // value hold index of to be deleted field 147 | return { 148 | ...state, 149 | xData: xData.filter((_, idx) => idx !== value), 150 | yData: yData.map(item => { 151 | const { data } = item 152 | const newData = data.filter((_, idx) => idx !== value) 153 | return { 154 | ...item, 155 | data: newData, 156 | error: areaChartErrorChecker(newData) 157 | } 158 | }) 159 | } 160 | case typeChange.deleteArea: 161 | // value is index of that area 162 | return { 163 | ...state, 164 | yData: yData.filter((_, idx) => idx !== value) 165 | } 166 | 167 | default: 168 | return state 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/barChartInput/reducer.ts: -------------------------------------------------------------------------------- 1 | import { isRealNumber, defaultFieldColor } from "../../../constants"; 2 | 3 | 4 | export enum typeChange { 5 | chartTitleChange, 6 | xLabelChange, 7 | yLabelChange, 8 | xDataFieldChange, 9 | addXFieldChange, 10 | removeXFieldChange, 11 | addYFieldChange, 12 | removeYFieldChange, 13 | xFieldChange, 14 | yFieldValueChange, 15 | yFieldNameChange, 16 | colorChange, 17 | } 18 | 19 | export interface BarchartState { 20 | chartTitle: string; 21 | xLabel: string; 22 | yLabel: string; 23 | xData: string[]; 24 | colors: string[]; 25 | yData: { 26 | error?: string; 27 | name: string; 28 | value: string; // when draw chart, we need to convert these strings to numbers 29 | }[][]; 30 | } 31 | 32 | export interface BarchartAction { 33 | type: typeChange; 34 | value?: any; 35 | options?: { 36 | index?: any; 37 | value?: any; 38 | }; 39 | } 40 | 41 | export const MAX_BLOCKS_PER_CHART = 15, MAX_COLLUMS_PER_BLOCK = 4 42 | 43 | type error = string | undefined 44 | function barChartErrorChecker(data: string): error { 45 | return !isRealNumber.test(data) ? "values need to be numbers" : undefined 46 | } 47 | 48 | export function barchartReducer(state: BarchartState, action: BarchartAction): BarchartState { 49 | // let newState = state; 50 | const { xData, yData, colors } = state 51 | const { type, value, options } = action 52 | 53 | switch (type) { 54 | case typeChange.chartTitleChange: 55 | state = { ...state, chartTitle: value } 56 | break 57 | case typeChange.xLabelChange: 58 | state = { ...state, xLabel: value } 59 | break 60 | case typeChange.yLabelChange: 61 | state = { ...state, yLabel: value } 62 | break 63 | case typeChange.xDataFieldChange: 64 | // value will be index, options.value will be value for that field 65 | xData[value] = options?.value 66 | state = { ...state, xData } 67 | break 68 | case typeChange.addXFieldChange: 69 | if (xData.length < MAX_BLOCKS_PER_CHART) { 70 | const { length } = yData[0] 71 | state = { 72 | ...state, 73 | xData: xData.concat(""), 74 | yData: [ 75 | ...yData, 76 | [...new Array(length)].map((_, idx) => { 77 | const { name } = yData[0][idx] 78 | return { 79 | name: name, 80 | value: "" 81 | } 82 | }) 83 | ] 84 | } 85 | } 86 | break 87 | case typeChange.removeXFieldChange: 88 | // value will be the index to remove 89 | state = { 90 | ...state, 91 | xData: xData.filter((_, idx) => idx !== value), 92 | yData: yData.filter((_, idx) => idx !== value) 93 | } 94 | break 95 | case typeChange.addYFieldChange: 96 | if (yData[0].length < MAX_COLLUMS_PER_BLOCK) { 97 | state = { 98 | ...state, 99 | yData: yData.map(block => { 100 | return block.concat({ 101 | name: "", 102 | value: "" 103 | }) 104 | }), 105 | colors: colors.concat(defaultFieldColor) 106 | } 107 | } 108 | break 109 | case typeChange.removeYFieldChange: 110 | // value is index of bar to remove in each block 111 | state = { 112 | ...state, 113 | yData: yData.map(block => { 114 | return block.filter((_, idx) => idx !== value) 115 | }), 116 | colors: colors.filter((_, idx) => idx !== value) 117 | } 118 | break 119 | case typeChange.xFieldChange: 120 | // value is index of x field to update, options.value is new value for that field 121 | state = { 122 | ...state, 123 | xData: xData.map((itm, idx) => { 124 | if (idx === value) { 125 | return options?.value 126 | } 127 | return itm 128 | }) 129 | } 130 | break 131 | case typeChange.yFieldValueChange: 132 | // value is index of y block, options.value is data for that, options.index is index of that field 133 | state = { 134 | ...state, 135 | yData: yData.map((block, blockIndex) => { 136 | if (blockIndex === value) { 137 | return block.map((bar, barIndex) => { 138 | if (barIndex === options?.index) { 139 | return { 140 | ...bar, 141 | value: options?.value, 142 | error: barChartErrorChecker(options?.value) 143 | } 144 | } 145 | return bar 146 | }) 147 | } 148 | return block 149 | }) 150 | } 151 | break 152 | case typeChange.yFieldNameChange: 153 | // value is index of bar, options.value is data for that block 154 | state = { 155 | ...state, 156 | yData: yData.map(block => { 157 | return block.map((bar, barIdx) => { 158 | if (barIdx === value) { 159 | return { 160 | ...bar, 161 | name: options?.value 162 | } 163 | } 164 | return bar 165 | }) 166 | }) 167 | } 168 | break 169 | case typeChange.colorChange: 170 | // value is bar index, options.value is bar color value 171 | state = { 172 | ...state, 173 | colors: state.colors.map((itm, idx) => { 174 | if (idx === value) { 175 | return options?.value 176 | } 177 | return itm 178 | }) 179 | } 180 | break 181 | 182 | default: 183 | break 184 | } 185 | 186 | return state; 187 | } 188 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/area.tsx: -------------------------------------------------------------------------------- 1 | import { useReactiveVar } from "@apollo/client" 2 | import React, { memo, useEffect, useState } from "react" 3 | import { localState } from "../.." 4 | import { updateLocalState } from "../../utils" 5 | import StdAreaChart, { AreaChartProps } from "../standardChart/area" 6 | 7 | 8 | function Area() { 9 | 10 | // get local chart state 11 | const { areaChartState, chartDrawMutexReleased } = useReactiveVar(localState) 12 | 13 | const [state, setState] = useState({ 14 | xLabels: [], 15 | xLabel: "", 16 | yLabel: "", 17 | chartTitle: "", 18 | yDataList: [] 19 | }) 20 | const { xLabels, yLabel, xLabel, chartTitle, yDataList } = state 21 | 22 | useEffect(() => { 23 | if (!!areaChartState && chartDrawMutexReleased) { 24 | const { xData, xLabel, yLabel, chartTitle, yData } = areaChartState 25 | const newAreaChartProps: AreaChartProps = { 26 | xLabels: xData, 27 | chartTitle, 28 | xLabel, 29 | yLabel, 30 | yDataList: yData.map(item => { 31 | const numberList = item.data.map(item => Number(item)) 32 | return { 33 | color: item.color, 34 | label: item.name, 35 | data: numberList 36 | } 37 | }) 38 | } 39 | setState(newAreaChartProps) 40 | updateLocalState("chartDrawMutexReleased", false) 41 | } 42 | }, [areaChartState, chartDrawMutexReleased]) 43 | 44 | return ( 45 |
46 | 53 |
54 | ) 55 | } 56 | 57 | export default memo(Area) -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/bar.tsx: -------------------------------------------------------------------------------- 1 | import { useReactiveVar } from "@apollo/client" 2 | import React, { memo, useEffect, useState } from "react" 3 | import { localState } from "../.." 4 | import StdBarchart, { BarChartProps } from "../standardChart/bar" 5 | import { updateLocalState } from "../../utils" 6 | 7 | 8 | function BarDrawer() { 9 | 10 | // get local chart state 11 | const { barChartState, chartDrawMutexReleased } = useReactiveVar(localState) 12 | 13 | const [state, setState] = useState({ 14 | xLabels: [], 15 | xLabel: "", 16 | yLabel: "", 17 | yDataList: [], 18 | chartTitle: "" 19 | }) 20 | const { xLabels, yDataList, chartTitle, xLabel, yLabel } = state 21 | 22 | useEffect(() => { 23 | if (!!barChartState && chartDrawMutexReleased) { 24 | const { 25 | chartTitle, 26 | yLabel: yLabel_, 27 | xLabel: xLabel_, 28 | xData, 29 | colors, 30 | yData 31 | } = barChartState 32 | let newDataList: { 33 | label?: string; 34 | color?: string; 35 | data: number[]; 36 | }[] = [] 37 | const { length } = yData[0] // get number of bars in a block 38 | for (let i = 0; i < length; i++) { 39 | let label = ""; 40 | const yDataListDataItem = yData.map(block => { 41 | label = block[i].name 42 | return Number(block[i].value) 43 | }) 44 | newDataList.push({ 45 | label, 46 | data: yDataListDataItem, 47 | color: colors[i] 48 | }) 49 | } 50 | setState({ 51 | chartTitle, 52 | xLabels: xData, 53 | xLabel: xLabel_, 54 | yLabel: yLabel_, 55 | yDataList: newDataList 56 | }) 57 | updateLocalState("chartDrawMutexReleased", false) 58 | } 59 | }, [barChartState, chartDrawMutexReleased]) 60 | 61 | return ( 62 |
63 | 70 |
71 | ) 72 | } 73 | 74 | export default memo(BarDrawer) -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/index.ts: -------------------------------------------------------------------------------- 1 | import BarChartDrawer from "./bar" 2 | import LineChartDrawer from "./line" 3 | import PieChartDrawer from "./pie" 4 | import AreaChartDrawer from "./area" 5 | import ScatterChartDrawer from "./scatter" 6 | 7 | 8 | export { 9 | BarChartDrawer, 10 | LineChartDrawer, 11 | PieChartDrawer, 12 | AreaChartDrawer, 13 | ScatterChartDrawer 14 | } 15 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/line.tsx: -------------------------------------------------------------------------------- 1 | import { useReactiveVar } from "@apollo/client" 2 | import React, { memo, useEffect, useState } from "react" 3 | import { LocalState } from "../.." 4 | import { localState } from "../../" 5 | import StdLineChart, { LineChartProps } from "../standardChart/line" 6 | import { updateLocalState } from "../../utils" 7 | 8 | 9 | function LineDrawer() { 10 | 11 | // get local chart state 12 | const { lineChartState, chartDrawMutexReleased } = useReactiveVar(localState) 13 | 14 | const [state, setState] = useState({ 15 | xLabels: [], 16 | yDataList: [], 17 | chartTitle: "", 18 | xLabel: "", 19 | yLabel: "" 20 | }) 21 | const { xLabels, yDataList, chartTitle, xLabel, yLabel } = state 22 | 23 | useEffect(() => { 24 | if (!!lineChartState && chartDrawMutexReleased) { 25 | const { chartTitle, xData, yData, xLabel, yLabel } = lineChartState 26 | const newLineChartProps: LineChartProps = { 27 | xLabels: xData, 28 | chartTitle, 29 | xLabel, 30 | yLabel, 31 | yDataList: yData.map(item => { 32 | const numberList = item.data.map(itm => Number(itm)) // NOTE: Number("") === 0 33 | return { 34 | color: item.color, 35 | label: item.name, 36 | data: numberList 37 | } 38 | }) 39 | } 40 | setState(newLineChartProps) 41 | updateLocalState("chartDrawMutexReleased", false) 42 | } 43 | }, [lineChartState, chartDrawMutexReleased]) 44 | 45 | return ( 46 |
47 | 54 |
55 | ) 56 | } 57 | 58 | export default memo(LineDrawer) -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/pie.tsx: -------------------------------------------------------------------------------- 1 | import { useReactiveVar } from "@apollo/client" 2 | import React, { memo, useEffect, useState } from "react" 3 | import { localState } from "../.." 4 | import StdPieChart, { StdPieChartProps } from "../standardChart/pie" 5 | import { updateLocalState } from "../../utils" 6 | 7 | 8 | function PieDrawer() { 9 | 10 | // get local chart state 11 | const { pieChartState, chartDrawMutexReleased } = useReactiveVar(localState) 12 | 13 | const [state, setState] = useState<{ 14 | pies: StdPieChartProps[], 15 | chartTitle: string; 16 | }>({ 17 | pies: [], 18 | chartTitle: "Chart title" 19 | }) 20 | const { pies, chartTitle } = state 21 | 22 | useEffect(() => { 23 | if (!!pieChartState && chartDrawMutexReleased) { 24 | const { chartTitle, pies } = pieChartState 25 | const newPies: StdPieChartProps[] = pies.map(pie => { 26 | const labels: string[] = [], 27 | sliceBackgrounds: string[] = [], 28 | data: number[] = [] 29 | 30 | pie.slices.forEach(slice => { 31 | labels.push(slice.name) 32 | sliceBackgrounds.push(slice.color) 33 | data.push(Number(slice.value)) 34 | }) 35 | 36 | return { 37 | labels, 38 | sliceBackgrounds, 39 | data, 40 | name: pie.name 41 | } 42 | }) 43 | 44 | setState({ 45 | pies: newPies, 46 | chartTitle 47 | }) 48 | updateLocalState("chartDrawMutexReleased", false) 49 | } 50 | 51 | }, [pieChartState, chartDrawMutexReleased]) 52 | 53 | return ( 54 |
55 |
56 | {chartTitle || "Chart title"} 57 |
58 |
59 | {pies.map((pie, pieIndex) => ( 60 |
1 ? "w-1/2 sm:w-full" : "w-full"}`} 63 | style={{ padding: 2 }} 64 | > 65 |
1 ? "border-solid border rounded border-gray-300" : ""}`}> 66 | 73 |
74 |
75 | ))} 76 |
77 |
78 | ) 79 | } 80 | 81 | export default memo(PieDrawer) -------------------------------------------------------------------------------- /src/__fixtures__/chart/chartDrawer/scatter.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | 4 | function Scatter() { 5 | return ( 6 |
scatter
7 | ) 8 | } 9 | 10 | export default memo(Scatter) -------------------------------------------------------------------------------- /src/__fixtures__/chart/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useMemo, useRef, useState } from "react" 2 | import { PieChart, Timeline, Save, Image, CloudUploadOutlined } from "@material-ui/icons" 3 | import { ChartBar, Scatter, AreaChart } from "../icons" 4 | import Tooltip from "@material-ui/core/Tooltip" 5 | import Button from "@material-ui/core/Button" 6 | import Menu from "../menu" 7 | import LineChartInput from "./lineChartInput" 8 | import BarChartInput from "./barChartInput" 9 | import PieChartInput from "./pieChartInput" 10 | import AreaChartInput from "./areaChartInput" 11 | import SimpleBar from "simplebar-react" 12 | import { ClickAwayListener, SvgIconTypeMap } from "@material-ui/core" 13 | import { OverridableComponent } from "@material-ui/core/OverridableComponent" 14 | import DummyChartInput from "./dummyInput/dummy" 15 | import { BarChartDrawer, LineChartDrawer, PieChartDrawer, ScatterChartDrawer, AreaChartDrawer } from "./chartDrawer" 16 | import html2canvas from "html2canvas" 17 | import "simplebar/dist/simplebar.min.css" 18 | import dayjs from "dayjs" 19 | import { Subscription, timer } from "rxjs" 20 | import { useTranslation } from "react-i18next" 21 | import { CSSTransition } from "react-transition-group" 22 | 23 | 24 | type saveType = 25 | | "image" 26 | | "cloud" 27 | 28 | function Chart() { 29 | 30 | // trans 31 | const { t } = useTranslation() 32 | 33 | // refs 34 | const menuRef = useRef() 35 | const chartDrawRef = useRef() 36 | const timerSub = useRef() 37 | 38 | // memoized values 39 | const chartRoutes = useMemo<{ 40 | name: string; 41 | icon: OverridableComponent>; 42 | tailwindColor: string; 43 | tailwindActiveBg: string; 44 | tailwindHoverBg: string; 45 | inputComponent: React.MemoExoticComponent<() => JSX.Element>; 46 | drawerComponent: React.MemoExoticComponent<() => JSX.Element>; 47 | afterBg: string; 48 | }[]>(() => { 49 | return [ 50 | { name: "chartType.bar", icon: ChartBar, tailwindColor: "text-green-500", tailwindActiveBg: "bg-green-200", tailwindHoverBg: "hover:bg-green-200", inputComponent: BarChartInput, drawerComponent: BarChartDrawer, afterBg: "border-green-200" }, 51 | { name: "chartType.pie", icon: PieChart, tailwindColor: "text-red-500", tailwindActiveBg: "bg-red-200", tailwindHoverBg: "hover:bg-red-200", inputComponent: PieChartInput, drawerComponent: PieChartDrawer, afterBg: "border-red-200" }, 52 | { name: "chartType.line", icon: Timeline, tailwindColor: "text-orange-500", tailwindActiveBg: "bg-orange-200", tailwindHoverBg: "hover:bg-orange-200", inputComponent: LineChartInput, drawerComponent: LineChartDrawer, afterBg: "border-orange-200" }, 53 | { name: "chartType.area", icon: AreaChart, tailwindColor: "text-purple-500", tailwindActiveBg: "bg-purple-200", tailwindHoverBg: "hover:bg-purple-200", inputComponent: AreaChartInput, drawerComponent: AreaChartDrawer, afterBg: "border-purple-200" }, 54 | { name: "chartType.scatter", icon: Scatter, tailwindColor: "text-blue-500", tailwindActiveBg: "bg-blue-200", tailwindHoverBg: "hover:bg-blue-200", inputComponent: DummyChartInput, drawerComponent: ScatterChartDrawer, afterBg: "border-blue-200" }, 55 | ] 56 | }, []) 57 | 58 | const saveChartMenuValues = useMemo<{ 59 | display: React.ReactNode, 60 | returnVal?: saveType; 61 | }[]>(() => [ 62 | { 63 | display: ( 64 |
65 | 66 | {t("saveChart.type.image")} 67 |
68 | ), 69 | returnVal: "image" 70 | }, 71 | { 72 | display: ( 73 |
74 | 75 | {t("saveChart.type.cloud")} 76 |
77 | ), 78 | returnVal: "cloud" 79 | } 80 | ], [t]) 81 | 82 | // component state 83 | const [state, setState] = useState({ 84 | activeIndex: 0, 85 | }) 86 | const { activeIndex } = state 87 | 88 | const changeChart = (newIdx: number) => () => { 89 | 90 | if (newIdx !== activeIndex) { 91 | setState({ 92 | ...state, 93 | activeIndex: newIdx 94 | }) 95 | } 96 | } 97 | 98 | const saveChartHandler = (type: saveType) => { 99 | html2canvas(chartDrawRef.current as HTMLDivElement) 100 | .then(canvas => { 101 | canvas.toBlob( 102 | (blob) => { 103 | const link = document.createElement("a") 104 | link.download = `chart-${dayjs().format("MM-DD-YYYY")}.png` 105 | const url = (URL || webkitURL).createObjectURL(blob) 106 | link.href = url 107 | document.body.appendChild(link) 108 | link.click() 109 | timerSub.current = timer(200).subscribe(() => { 110 | document.body.removeChild(link); 111 | (URL || webkitURL).revokeObjectURL(url) 112 | }) 113 | }, 114 | "image/png", 115 | 1 116 | ) 117 | }) 118 | .catch(console.error) 119 | } 120 | 121 | useEffect(() => { 122 | return () => { 123 | timerSub.current?.unsubscribe() 124 | } 125 | }, []) 126 | 127 | const CurrentDrawer = chartRoutes[activeIndex].drawerComponent 128 | 129 | return ( 130 |
131 | {/* chart result */} 132 |
133 |
134 | {/* display type of chart */} 135 |

{t(chartRoutes[activeIndex].name)}

136 | { 138 | (menuRef.current as HTMLElement).classList.add("hidden") 139 | }} 140 | > 141 |
142 | 155 | 161 |
162 |
163 |
164 |
165 |
166 | {/* chart display area */} 167 | 168 |
169 |
170 |
171 | 172 | {/* chart input field */} 173 |
174 |
175 | {t("chartInput.headName")} 176 |
177 |
178 |
179 | {/* chart input type switcher */} 180 |
181 | {chartRoutes.map((item, idx) => ( 182 | 187 |
191 | 192 | {activeIndex === idx && } 193 |
194 |
195 | ))} 196 |
197 | 203 | {chartRoutes.map((item, idx) => ( 204 | 211 | 212 | 213 | ))} 214 | 215 |
216 |
217 |
218 |
219 | ) 220 | } 221 | 222 | export default memo(Chart) 223 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/lineChartInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useReducer } from "react" 2 | import DelayInput from "../../delayinput" 3 | import { Add, Remove } from "@material-ui/icons" 4 | import Tooltip from "@material-ui/core/Tooltip" 5 | import { Button } from "@material-ui/core" 6 | import { 7 | lineChartReducer, LineChartAction, 8 | LineChartState, typeChange, MAX_LINES, 9 | } from "./reducer" 10 | import ColorSetter from "../../colorSetter" 11 | import { localState } from "../.." 12 | import { noAnyError } from "../../utils" 13 | import { useTranslation } from "react-i18next" 14 | 15 | 16 | function LineChart(): JSX.Element { 17 | 18 | // trans 19 | const { t } = useTranslation() 20 | 21 | // component state 22 | const [state, dispatch] = useReducer>(lineChartReducer, localState().lineChartState) 23 | const { chartTitle, xData, yData, xLabel, yLabel } = state 24 | 25 | useEffect(() => { 26 | if (noAnyError(state.yData.map(line => line.error))) { 27 | localState({ 28 | ...localState(), 29 | lineChartState: state, 30 | chartDrawMutexReleased: true 31 | }) 32 | } 33 | }, [state]) 34 | 35 | return ( 36 |
37 | {/* chart title */} 38 |
39 | 40 | dispatch({ 47 | type: typeChange.titleChange, 48 | value 49 | })} 50 | /> 51 |
52 | 53 |
54 |
55 | {/* title on Ox */} 56 |
57 | 58 | { 65 | dispatch({ 66 | type: typeChange.xLabelChange, 67 | value: value 68 | }) 69 | }} 70 | /> 71 |
72 | 73 | {/* x data */} 74 |
75 | 76 | {t("chartInput.dataOnOx")} 77 | 78 | {xData.map((item, idx) => ( 79 |
80 | {idx + 1} 81 | dispatch({ 88 | type: typeChange.xFieldChange, 89 | value: idx, 90 | options: { 91 | value 92 | } 93 | })} 94 | /> 95 | 99 |
{ 102 | dispatch({ 103 | type: typeChange[!idx ? "addXField" : "deleteXField"], 104 | value: idx 105 | }) 106 | }} 107 | > 108 | {!idx ? : } 109 |
110 |
111 |
112 | ))} 113 |
114 |
115 | 116 |
117 | {/* title on oy */} 118 |
119 |
120 | 121 | { 128 | dispatch({ 129 | type: typeChange.yLabelChange, 130 | value: value.trim() 131 | }) 132 | }} 133 | /> 134 |
135 |
136 | 137 | {/* Data on Oy */} 138 |
139 | 140 | {t("chartInput.dataOnOy")} 141 | 142 | {yData.map((item, index) => ( 143 |
147 | 148 | dispatch({ 153 | type: typeChange.lineNameChange, 154 | value: index, 155 | options: { 156 | value 157 | } 158 | })} 159 | defaultValue={item.name} 160 | placeholder={`${t("chartInput.line.placeholder.lineName")}`} 161 | endAdornment={( 162 | { 164 | if (color !== item.color) { // only update state if colors don't match 165 | dispatch({ 166 | type: typeChange.colorChange, 167 | value: index, 168 | options: { 169 | value: color 170 | } 171 | }) 172 | } 173 | }} 174 | defaultBg={item.color} 175 | /> 176 | )} 177 | /> 178 | 179 | {item.data.map((field, idx) => ( 180 |
184 | {idx + 1} 185 | dispatch({ 191 | type: typeChange.yFieldChange, 192 | value: index, 193 | options: { 194 | value, 195 | index: idx 196 | } 197 | })} 198 | defaultValue={field} 199 | /> 200 |
201 | ))} 202 | {!!item.error && {item.error}} 203 | {!!index && ( 204 |
205 | 221 |
222 | )} 223 |
224 | ))} 225 | 239 |
240 |
241 |
242 |
243 | ) 244 | } 245 | 246 | export default memo(LineChart) 247 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/lineChartInput/reducer.ts: -------------------------------------------------------------------------------- 1 | import { isRealNumber, defaultFieldColor } from "../../../constants" 2 | 3 | 4 | export interface LineChartState { 5 | chartTitle: string; 6 | xData: string[]; 7 | xLabel: string; 8 | yLabel: string; 9 | yData: { 10 | color: string; 11 | name: string; 12 | data: string[]; 13 | error?: string; 14 | }[]; 15 | } 16 | 17 | export enum typeChange { 18 | titleChange, 19 | addXField, 20 | deleteXField, 21 | addLine, 22 | deleteLine, 23 | xFieldChange, 24 | yFieldChange, 25 | lineNameChange, 26 | colorChange, 27 | xLabelChange, 28 | yLabelChange, 29 | } 30 | 31 | export interface LineChartAction { 32 | type: typeChange; 33 | value?: any; 34 | options?: { 35 | index?: any; 36 | value?: any; 37 | } 38 | } 39 | 40 | // constants 41 | export const MAX_POINTS = 25, MAX_LINES = 5 42 | 43 | type error = string | undefined 44 | function lineChartErrorChecker(data: string[]): error { 45 | return data.some(item => !isRealNumber.test(item)) ? "values need to be numbers" : undefined 46 | } 47 | 48 | export function lineChartReducer(state: LineChartState, action: LineChartAction): LineChartState { 49 | const { type, value, options } = action 50 | const { xData, yData } = state 51 | 52 | switch (type) { 53 | case typeChange.titleChange: 54 | state = { ...state, chartTitle: value } 55 | break 56 | case typeChange.addXField: 57 | if (xData.length < MAX_POINTS) { 58 | state = { 59 | ...state, 60 | xData: xData.concat(""), 61 | yData: yData.map(item => { 62 | return { 63 | ...item, 64 | data: item.data.concat("") 65 | } 66 | }) 67 | } 68 | } 69 | break 70 | case typeChange.deleteXField: 71 | // value hold index of to be deleted field 72 | state = { 73 | ...state, 74 | xData: [ 75 | ...xData.slice(0, value), 76 | ...xData.slice(value + 1) 77 | ], 78 | yData: yData.map(item => { 79 | const { data } = item 80 | const newData = [ 81 | ...data.slice(0, value), 82 | ...data.slice(value + 1) 83 | ] 84 | return { 85 | ...item, 86 | data: newData, 87 | error: lineChartErrorChecker(newData) 88 | } 89 | }) 90 | } 91 | break 92 | case typeChange.addLine: 93 | state = { 94 | ...state, 95 | yData: yData.concat({ 96 | name: "", 97 | data: [...(new Array(xData.length))].map(() => ""), 98 | color: defaultFieldColor 99 | }) 100 | } 101 | break 102 | case typeChange.deleteLine: 103 | // value is index of line to be deleted 104 | state = { 105 | ...state, 106 | yData: [ 107 | ...yData.slice(0, value), 108 | ...yData.slice(value + 1) 109 | ] 110 | } 111 | break 112 | case typeChange.xFieldChange: 113 | // value is index of the x field, options.value is value for it 114 | state = { 115 | ...state, 116 | xData: xData.map((itm, idx) => { 117 | if (idx === value) { 118 | return options?.value 119 | } 120 | return itm 121 | }) 122 | } 123 | break 124 | case typeChange.yFieldChange: 125 | // value is index of line, options.index is index of y field, options.value is value for it 126 | state = { 127 | ...state, 128 | yData: yData.map((item, index) => { 129 | if (value === index) { 130 | const data = item.data.map((itm, idx) => { 131 | if (idx === options?.index) { 132 | return options.value 133 | } 134 | return itm 135 | }) 136 | return { 137 | ...item, 138 | data, 139 | error: lineChartErrorChecker(data) 140 | } 141 | } 142 | return item 143 | }) 144 | } 145 | break 146 | case typeChange.lineNameChange: 147 | // value is index, options.value is value for it 148 | state = { 149 | ...state, 150 | yData: yData.map((item, idex) => { 151 | if (idex === value) { 152 | return { 153 | ...item, 154 | name: options?.value 155 | } 156 | } 157 | return item 158 | }) 159 | } 160 | break 161 | case typeChange.colorChange: 162 | // value is line index, options.value is color for that line 163 | state = { 164 | ...state, 165 | yData: yData.map((item, idx) => { 166 | if (idx === value) { 167 | return { 168 | ...item, 169 | color: options?.value 170 | } 171 | } 172 | return item 173 | }) 174 | } 175 | break 176 | case typeChange.xLabelChange: 177 | // action.value is new value for X label 178 | state = { ...state, xLabel: value } 179 | break 180 | case typeChange.yLabelChange: 181 | // action.value is new value for Y label 182 | state = { ...state, yLabel: value } 183 | break 184 | 185 | default: 186 | break 187 | } 188 | 189 | return state 190 | } -------------------------------------------------------------------------------- /src/__fixtures__/chart/pieChartInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useReducer } from "react" 2 | import DelayInput from "../../delayinput" 3 | import Button from "@material-ui/core/Button" 4 | import { DeleteOutlined } from "@material-ui/icons" 5 | import Tooltip from "@material-ui/core/Tooltip" 6 | import { 7 | PieChartAction, 8 | PieChartState, 9 | typeChange, 10 | MAX_PIE, 11 | pieChartReducer, 12 | } from "./reducer" 13 | import ColorSettter from "../../colorSetter" 14 | import { localState } from "../.." 15 | import { noAnyError } from "../../utils" 16 | import { useTranslation } from "react-i18next" 17 | 18 | 19 | function PieChart(): JSX.Element { 20 | 21 | // trans 22 | const { t } = useTranslation() 23 | 24 | // component state 25 | const [state, dispatch] = useReducer>(pieChartReducer, localState().pieChartState) 26 | let { chartTitle, pies } = state 27 | 28 | useEffect(() => { 29 | // to tell current chart drawer to re-draw chart 30 | // since React notifies errors if drawer component and input component update simultaneously 31 | if (noAnyError( 32 | state.pies 33 | .map(pie => pie.slices.map(slice => slice.error)) 34 | .reduce((a, b) => a.concat(b), []) 35 | )) { 36 | localState({ 37 | ...localState(), 38 | chartDrawMutexReleased: true, 39 | pieChartState: state 40 | }) 41 | } 42 | }, [state]) 43 | 44 | return ( 45 |
46 | {/* title */} 47 |
48 | 49 | dispatch({ 54 | type: typeChange.titleChange, 55 | value 56 | })} 57 | defaultValue={chartTitle} 58 | /> 59 |
60 | 61 |
62 | {pies.map((pie, pieIndex) => ( 63 |
67 | 68 | {`${t("chartInput.pie.pie")} ${pie.name || pieIndex + 1}`} 69 | 70 | {/* pie name */} 71 |
72 | {t("chartInput.pie.pieName")} 73 | dispatch({ 75 | type: typeChange.pieNameChange, 76 | value: pieIndex, 77 | options: { 78 | value 79 | } 80 | })} 81 | placeholder={`${t("chartInput.pie.placeholder.enterPieName")}`} 82 | defaultValue={pie.name} 83 | className={`rounded bg-gray-200 px-2`} 84 | /> 85 |
86 | {pie.slices.map((slice, sliceIndex) => ( 87 |
88 |
89 | 90 | {t("chartInput.pie.slice")} {sliceIndex + 1} 91 | 92 | {!pieIndex ? ( 93 |
94 | {t("chartInput.pie.sliceName")} 95 | dispatch({ 101 | type: typeChange.sliceNameChange, 102 | value: sliceIndex, 103 | options: { 104 | value 105 | } 106 | })} 107 | disabled={!!pieIndex} 108 | endAdornment={pieIndex === 0 && ( 109 | dispatch({ 111 | type: typeChange.colorChange, 112 | value: sliceIndex, 113 | options: { 114 | value: color 115 | } 116 | })} 117 | defaultBg={slice.color} 118 | /> 119 | )} 120 | /> 121 |
122 | ) : ( 123 |
124 | {t("chartInput.pie.sliceName")} 125 | {slice.name} 126 |
127 | )} 128 |
129 | {t("chartInput.pie.value")} 130 | dispatch({ 135 | type: typeChange.sliceValueChange, 136 | value: pieIndex, 137 | options: { 138 | index: sliceIndex, 139 | value 140 | } 141 | })} 142 | defaultValue={slice.value} 143 | /> 144 |
145 | {!!slice.error && {slice.error}} 146 |
147 | {!!sliceIndex && !pieIndex && ( 148 | // only display this button in fields that has pieIndex greater than 0 149 |
150 | 154 | dispatch({ 157 | type: typeChange.deleteSlice, 158 | value: sliceIndex 159 | })} 160 | > 161 | 162 | 163 | 164 |
165 | )} 166 |
167 | ))} 168 | 182 |
183 | ))} 184 | 198 |
199 |
200 | ) 201 | } 202 | 203 | export default memo(PieChart) 204 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/pieChartInput/reducer.ts: -------------------------------------------------------------------------------- 1 | import { isRealNumber, defaultFieldColor } from "../../../constants" 2 | 3 | 4 | export enum typeChange { 5 | addPie, 6 | deletePie, 7 | titleChange, 8 | addSlice, 9 | deleteSlice, 10 | sliceNameChange, 11 | sliceValueChange, 12 | colorChange, 13 | pieNameChange, 14 | } 15 | 16 | export interface PieChartState { 17 | chartTitle: string; 18 | pies: { 19 | name: string; 20 | slices: { 21 | color: string; 22 | name: string; 23 | value: string; 24 | error?: string; 25 | }[] 26 | }[]; 27 | } 28 | 29 | export interface PieChartAction { 30 | type: typeChange; 31 | value?: any; 32 | options?: { 33 | index?: any; 34 | value?: any; 35 | }; 36 | } 37 | 38 | // constants 39 | export const MAX_SLICES_PER_PIE = 15, MAX_PIE = 4 40 | 41 | export function pieChartReducer(state: PieChartState, action: PieChartAction): PieChartState { 42 | const { pies } = state 43 | const { type, value, options } = action 44 | 45 | let clonePies = [...pies] 46 | 47 | switch (type) { 48 | case typeChange.addPie: 49 | if (pieChartReducer.length < MAX_PIE) { 50 | state = { 51 | ...state, 52 | pies: clonePies.concat({ 53 | name: "", 54 | slices: clonePies[0].slices.map(slice => { 55 | return { 56 | color: slice.color, // borrow color from the slice at this index of the first pie 57 | name: slice.name, // borrow name from the slice at this index of the first pie 58 | value: "", 59 | } 60 | }) 61 | }) 62 | } 63 | } 64 | break 65 | case typeChange.deletePie: 66 | // value holds index of pie to remove 67 | state = { ...state, pies: clonePies.filter((_, idx) => idx !== value) } 68 | break 69 | case typeChange.titleChange: 70 | state = { ...state, chartTitle: action.value } 71 | break 72 | case typeChange.addSlice: 73 | if (pies[0].slices.length < MAX_SLICES_PER_PIE) { 74 | state = { 75 | ...state, 76 | pies: clonePies.map(pie => { 77 | return { 78 | ...pie, 79 | slices: pie.slices.concat({ 80 | name: "", 81 | value: "", 82 | error: undefined, 83 | color: defaultFieldColor 84 | }) 85 | } 86 | }) 87 | } 88 | } 89 | break 90 | case typeChange.deleteSlice: 91 | // value hold index to remove 92 | state = { 93 | ...state, 94 | pies: clonePies.map(pie => { 95 | return { 96 | ...pie, 97 | slices: pie.slices.filter((_, id) => id !== value) 98 | } 99 | }) 100 | } 101 | break 102 | case typeChange.sliceNameChange: 103 | // value is index, options.value is new name for that slice 104 | state = { 105 | ...state, 106 | pies: clonePies.map(pie => { 107 | return { 108 | ...pie, 109 | slices: pie.slices.map((slice, idx) => { 110 | const { name } = slice 111 | return { 112 | ...slice, 113 | name: idx === value ? options?.value : name, 114 | } 115 | }) 116 | } 117 | }) 118 | } 119 | break 120 | case typeChange.sliceValueChange: 121 | // value is index of pie to update, options.index is index of that slice, options.value is value for that slice 122 | // check if the input value is a real number: 123 | let error = isRealNumber.test(options?.value) ? undefined : "value must be a number" 124 | 125 | clonePies = clonePies.map((pie, pieIdx) => { 126 | if (value === pieIdx) { 127 | return { 128 | ...pie, 129 | slices: pie.slices.map((slice, sliceIdx) => { 130 | if (sliceIdx === options?.index) { 131 | return { ...slice, error, value: options.value } 132 | } 133 | return slice 134 | }) 135 | } 136 | } 137 | return pie 138 | }) 139 | 140 | state = { ...state, pies: clonePies } 141 | break 142 | case typeChange.colorChange: 143 | // value is slice indexes, options.value is color for those slices 144 | clonePies = clonePies.map(pie => { 145 | return { 146 | ...pie, 147 | slices: pie.slices.map((slice, sliceIndex) => { 148 | if (sliceIndex === value) { 149 | return { ...slice, color: options?.value } 150 | } 151 | return slice 152 | }) 153 | } 154 | }) 155 | state = { ...state, pies: clonePies } 156 | break 157 | 158 | case typeChange.pieNameChange: 159 | // value is pie index, options.value is new name for that pie 160 | clonePies = clonePies.map((pie, pieIndex) => { 161 | if (pieIndex === value) { 162 | return { 163 | ...pie, 164 | name: options?.value 165 | } 166 | } 167 | return pie 168 | }) 169 | state = { ...state, pies: clonePies } 170 | break 171 | 172 | default: 173 | break 174 | } 175 | 176 | return state 177 | } 178 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/scatterChartInput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | 3 | 4 | function ScatterChartInput() { 5 | return ( 6 |
7 | ) 8 | } 9 | 10 | export default memo(ScatterChartInput) 11 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/scatterChartInput/reducer.ts: -------------------------------------------------------------------------------- 1 | 2 | export function scatterChartReducer() { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/CommonStandard.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useRef } from "react" 2 | import Chart, { ChartConfiguration } from "chart.js" 3 | 4 | 5 | export interface CommonStdChartProps { 6 | config: ChartConfiguration; 7 | } 8 | 9 | function CommonStdChart({ config }: CommonStdChartProps) { 10 | 11 | // references 12 | const canvasRef = useRef() 13 | const chartRef = useRef() 14 | 15 | useEffect(() => { 16 | chartRef.current?.destroy() 17 | chartRef.current = new Chart( 18 | (canvasRef.current as HTMLCanvasElement).getContext("2d") as CanvasRenderingContext2D, 19 | config 20 | ) 21 | 22 | return () => { 23 | // remove chart data on canvas 24 | chartRef.current?.destroy() 25 | } 26 | }, [config]) 27 | 28 | return ( 29 | 32 | 33 | ) 34 | } 35 | 36 | export default memo(CommonStdChart) 37 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/area.tsx: -------------------------------------------------------------------------------- 1 | import { ChartConfiguration, ChartDataSets } from "chart.js" 2 | import React, { memo } from "react" 3 | import { defaultColors } from "../../../constants" 4 | import CommonStdChart from "./CommonStandard" 5 | 6 | 7 | export interface AreaChartProps { 8 | chartTitle?: string; 9 | xLabel?: string; 10 | xLabels: string[]; 11 | yLabel?: string; 12 | yDataList: { 13 | color?: string; 14 | label?: string; 15 | data: number[]; 16 | }[]; 17 | } 18 | 19 | function areaChartConfig({ chartTitle, xLabel, yLabel, xLabels, yDataList }: AreaChartProps): ChartConfiguration { 20 | const { length } = defaultColors 21 | const dtSets: ChartDataSets[] = yDataList.map((area, idx) => { 22 | const { color, label, data } = area // color should be rgb 23 | 24 | return { 25 | backgroundColor: (color || defaultColors[idx % length]), 26 | // .replace("rgb(", "rgba(").replace(")", ", 0.3)"), 27 | borderColor: "#fff", 28 | data, 29 | borderWidth: 1.5, 30 | lineTension: 0.0001, 31 | label, 32 | pointRadius: 1.5, 33 | fill: true 34 | } 35 | }) 36 | 37 | return { 38 | type: "line", 39 | data: { 40 | labels: xLabels, 41 | datasets: dtSets 42 | }, 43 | options: { 44 | spanGaps: false, 45 | responsive: true, 46 | elements: { 47 | line: { 48 | tension: 0.000001 49 | } 50 | }, 51 | legend: { 52 | align: "start", 53 | position: "bottom", 54 | fullWidth: false, 55 | labels: { 56 | boxWidth: 12, 57 | } 58 | }, 59 | animation: { 60 | duration: 500, 61 | easing: "linear" 62 | }, 63 | title: { 64 | display: true, 65 | text: chartTitle || "Area chart" 66 | }, 67 | scales: { 68 | xAxes: [{ 69 | ticks: { 70 | beginAtZero: true 71 | }, 72 | scaleLabel: { 73 | display: true, 74 | labelString: xLabel || "Label on Ox" 75 | } 76 | }], 77 | yAxes: [{ 78 | stacked: true, 79 | gridLines: { 80 | drawBorder: false 81 | }, 82 | ticks: { 83 | beginAtZero: true, 84 | max: 100, 85 | }, 86 | scaleLabel: { 87 | display: true, 88 | labelString: yLabel || "Label on Oy" 89 | } 90 | }] 91 | }, 92 | }, 93 | } 94 | } 95 | 96 | function StdAreaChart(props: AreaChartProps) { 97 | return ( 98 | 101 | ) 102 | } 103 | 104 | export default memo(StdAreaChart) 105 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/bar.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { ChartConfiguration, ChartDataSets } from "chart.js"; 3 | import { defaultColors } from "../../../constants"; 4 | import CommonStdChart from "./CommonStandard" 5 | 6 | 7 | export interface BarChartProps { 8 | xLabels: string[]; 9 | xLabel?: string; 10 | yLabel?: string; 11 | yDataList: { 12 | label?: string; 13 | color?: string; 14 | data: number[]; 15 | }[]; 16 | chartTitle?: string; 17 | } 18 | 19 | function barChartConfig({ xLabels, yDataList, chartTitle, xLabel, yLabel }: BarChartProps): ChartConfiguration { 20 | 21 | const dtSets: ChartDataSets[] = yDataList.map((bar, idx) => { 22 | const { label, color, data } = bar 23 | return { 24 | label: label || `Bar ${idx + 1}`, 25 | backgroundColor: color || defaultColors[idx % defaultColors.length], 26 | borderWidth: 0, 27 | data, 28 | // maxBarThickness: 20 29 | } 30 | }) 31 | 32 | return { 33 | type: "bar", 34 | data: { 35 | labels: xLabels, 36 | datasets: dtSets 37 | }, 38 | options: { 39 | responsive: true, 40 | legend: { 41 | align: "start", 42 | position: "bottom", 43 | fullWidth: false, 44 | labels: { 45 | boxWidth: 12, 46 | } 47 | }, 48 | animation: { 49 | duration: 500, 50 | easing: "linear" 51 | }, 52 | title: { 53 | display: true, 54 | text: chartTitle || "Bar Chart" 55 | }, 56 | scales: { 57 | xAxes: [{ 58 | ticks: { 59 | beginAtZero: true 60 | }, 61 | scaleLabel: { 62 | display: true, 63 | labelString: xLabel || "Label on Ox" 64 | } 65 | }], 66 | yAxes: [{ 67 | gridLines: { 68 | drawBorder: false 69 | }, 70 | ticks: { 71 | beginAtZero: true 72 | }, 73 | scaleLabel: { 74 | display: true, 75 | labelString: yLabel || "Label on Oy" 76 | } 77 | }] 78 | }, 79 | }, 80 | } 81 | } 82 | 83 | function StdBarchart(props: BarChartProps) { 84 | 85 | return ( 86 | 89 | ) 90 | } 91 | 92 | // export default memo(function () { 93 | // return 106 | // }) 107 | 108 | export default memo(StdBarchart) 109 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/line.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { ChartConfiguration, ChartDataSets } from "chart.js" 3 | import { defaultColors } from "../../../constants" 4 | import CommonStdChart from "./CommonStandard" 5 | 6 | 7 | export interface LineChartProps { 8 | xLabels: string[]; 9 | xLabel?: string; 10 | yLabel?: string; 11 | yDataList: { 12 | color?: string; 13 | label?: string; 14 | data: number[]; 15 | }[]; 16 | chartTitle?: string; 17 | } 18 | 19 | export function lineChartConfig({ xLabels, yDataList, chartTitle, xLabel, yLabel }: LineChartProps): ChartConfiguration { 20 | 21 | const dtSets: ChartDataSets[] = yDataList.map((line, idx) => { 22 | const { color, label, data } = line 23 | return { 24 | label: label || `Line ${idx + 1}`, 25 | borderColor: color || defaultColors[idx % defaultColors.length], 26 | data, 27 | pointRadius: 1, 28 | fill: false, 29 | lineTension: 0.2, 30 | borderWidth: 2, 31 | } 32 | }) 33 | 34 | return { 35 | type: "line", 36 | data: { 37 | labels: xLabels, 38 | datasets: dtSets, 39 | }, 40 | options: { 41 | responsive: true, 42 | legend: { 43 | align: "start", 44 | position: "bottom", 45 | fullWidth: false, 46 | labels: { 47 | boxWidth: 12, 48 | } 49 | }, 50 | animation: { 51 | duration: 500, 52 | easing: "linear" 53 | }, 54 | title: { 55 | display: true, 56 | text: chartTitle || "Line Chart" 57 | }, 58 | scales: { 59 | xAxes: [{ 60 | ticks: { 61 | beginAtZero: true 62 | }, 63 | scaleLabel: { 64 | display: true, 65 | labelString: xLabel || "Label on Ox", 66 | }, 67 | offset: true 68 | }], 69 | yAxes: [{ 70 | // gridLines: { 71 | // drawBorder: false 72 | // }, 73 | ticks: { 74 | beginAtZero: true 75 | }, 76 | scaleLabel: { 77 | display: true, 78 | labelString: yLabel || "Label on Oy", 79 | } 80 | }] 81 | }, 82 | tooltips: { 83 | intersect: false, 84 | mode: "index", 85 | } 86 | } 87 | } 88 | } 89 | 90 | function StdLineChart(props: LineChartProps) { 91 | 92 | return ( 93 | 96 | ) 97 | } 98 | 99 | // export default memo(function () { 100 | // return 115 | // }) 116 | export default memo(StdLineChart) 117 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/pie.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import { ChartConfiguration, ChartDataSets } from "chart.js"; 3 | import { defaultColors } from "../../../constants" 4 | import CommonStdChart from "./CommonStandard" 5 | 6 | 7 | export interface StdPieChartProps { 8 | labels: string[]; 9 | sliceBackgrounds?: string[]; 10 | data: number[]; 11 | name?: string; 12 | } 13 | 14 | export function pieChartConfig({ labels, sliceBackgrounds, data, name }: StdPieChartProps): ChartConfiguration { 15 | 16 | const { length } = defaultColors 17 | 18 | const dtSets: ChartDataSets[] = [{ 19 | data, 20 | backgroundColor: sliceBackgrounds || (new Array(data.length)) 21 | .fill(null) 22 | .map((_, idx) => defaultColors[idx % length]), 23 | }] 24 | 25 | return { 26 | type: "doughnut", 27 | data: { 28 | datasets: dtSets, 29 | labels, 30 | }, 31 | options: { 32 | responsive: true, 33 | legend: { 34 | align: "start", 35 | position: "bottom", 36 | fullWidth: false, 37 | labels: { 38 | boxWidth: 12, 39 | } 40 | }, 41 | animation: { 42 | duration: 500, 43 | easing: "linear", 44 | animateScale: true, 45 | animateRotate: true 46 | }, 47 | title: { 48 | display: true, 49 | text: name || "Pie chart", 50 | } 51 | }, 52 | } 53 | } 54 | 55 | function StdPieChart(props: StdPieChartProps) { 56 | 57 | return ( 58 | 61 | ) 62 | } 63 | 64 | export default memo(StdPieChart) 65 | -------------------------------------------------------------------------------- /src/__fixtures__/chart/standardChart/scatter.tsx: -------------------------------------------------------------------------------- 1 | import { ChartConfiguration } from "chart.js" 2 | import React, { memo } from "react" 3 | import CommonStdChart from "./CommonStandard" 4 | 5 | 6 | export interface StdScatterChartProps { 7 | name?: string; 8 | } 9 | 10 | export function scatterChartConfig({ name }: StdScatterChartProps): ChartConfiguration { 11 | return { 12 | type: "scatter", 13 | options: { 14 | responsive: true, 15 | legend: { 16 | align: "start", 17 | position: "bottom", 18 | fullWidth: false, 19 | labels: { 20 | boxWidth: 12, 21 | } 22 | }, 23 | animation: { 24 | duration: 500, 25 | easing: "linear", 26 | animateScale: true, 27 | animateRotate: true 28 | }, 29 | title: { 30 | display: true, 31 | text: name || "Scatter chart" 32 | } 33 | } 34 | } 35 | } 36 | 37 | function ScatterStdChart(props: StdScatterChartProps) { 38 | return ( 39 | 42 | ) 43 | } 44 | 45 | export default memo(ScatterStdChart) 46 | -------------------------------------------------------------------------------- /src/__fixtures__/chartCard/index.tsx: -------------------------------------------------------------------------------- 1 | import { DeleteOutlined, MoreHoriz, Schedule } from "@material-ui/icons" 2 | import React, { memo, useRef } from "react" 3 | import dayjs from "dayjs" 4 | import { ClickAwayListener } from "@material-ui/core" 5 | import { useTranslation } from "react-i18next" 6 | 7 | 8 | export interface ChartCardProps { 9 | img: string; 10 | title: string; 11 | timestamp: string; 12 | } 13 | 14 | function ChartCard({ img, title, timestamp }: ChartCardProps) { 15 | 16 | // trans 17 | const { t } = useTranslation() 18 | 19 | const deleteRef = useRef() 20 | 21 | const toggleDelete = (act: "open" | "close") => () => { 22 | (deleteRef.current as HTMLElement).style.display = act === "open" ? "flex" : "none" 23 | } 24 | 25 | return ( 26 |
27 | {/* chart image */} 28 |
34 |
35 | {/* meta */} 36 |
37 |
38 |

39 | {`${title.slice(0, 25)}...`} 40 |

41 |

42 | 43 | {/* refer to https://day.js.org/docs/en/display/format#list-of-localized-formats */} 44 | {dayjs(timestamp).format("MMM D, YYYY")} 45 |

46 |
47 | 48 |
49 |
52 | 53 |
54 |
58 | 59 | 60 | {t("delete")} 61 | 62 |
63 |
64 |
65 |
66 |
67 | ) 68 | } 69 | 70 | export default memo(ChartCard) 71 | -------------------------------------------------------------------------------- /src/__fixtures__/colorSetter/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from "react" 2 | import { CompactPicker, ColorResult } from "react-color" 3 | import Popover from "@material-ui/core/Popover" 4 | import { Tooltip } from "@material-ui/core" 5 | import { useTranslation } from "react-i18next" 6 | 7 | 8 | export interface ColorSetterProps { 9 | giveColor: (color: string) => void; 10 | defaultBg?: string; 11 | } 12 | 13 | function ColorSetter({ giveColor, defaultBg }: ColorSetterProps) { 14 | 15 | const { t } = useTranslation() 16 | 17 | const [state, setState] = useState<{ 18 | anchor: HTMLElement | null; 19 | bgColor: string; 20 | }>({ 21 | anchor: null, 22 | bgColor: defaultBg || "orange" 23 | }) 24 | const { anchor, bgColor } = state 25 | 26 | const changeColor = (color: ColorResult, evt: any) => { 27 | const { rgb: { r, g, b } } = color 28 | const cl = `rgb(${r}, ${g}, ${b})` 29 | setState({ 30 | ...state, 31 | bgColor: cl, 32 | }) 33 | giveColor(cl) 34 | } 35 | 36 | return ( 37 | <> 38 | 39 |
) => setState({ 42 | ...state, 43 | anchor: evt.currentTarget 44 | })} 45 | style={{ 46 | backgroundColor: bgColor 47 | }} 48 | > 49 |
50 |
51 | setState({ 55 | ...state, 56 | anchor: null 57 | })} 58 | anchorOrigin={{ 59 | vertical: "center", 60 | horizontal: "center", 61 | }} 62 | transformOrigin={{ 63 | vertical: "top", 64 | horizontal: "center", 65 | }} 66 | > 67 | 71 | 72 | 73 | ) 74 | } 75 | 76 | export default memo(ColorSetter) -------------------------------------------------------------------------------- /src/__fixtures__/delayinput/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useRef } from "react"; 2 | import InputBase from "@material-ui/core/InputBase"; 3 | import { debounceTime } from "rxjs/operators"; 4 | import { fromEvent, Subscription } from "rxjs"; 5 | import TextareaAutosize from '@material-ui/core/TextareaAutosize' 6 | 7 | 8 | export interface DelayInputParam { 9 | delay?: number, 10 | giveValue: (evt: React.MouseEvent) => void, 11 | } 12 | /** 13 | * @param {{ delay: Number, giveValue: Function, component?: 'textarea' | undefined }} param 14 | */ 15 | function DelayInput({ 16 | delay = 700, 17 | giveValue, 18 | component, 19 | ...rest 20 | }: any): JSX.Element { 21 | 22 | // references 23 | const inputRef = useRef(); 24 | const subscriptionRef = useRef(); 25 | 26 | // fires after mounting: 27 | useEffect(() => { 28 | const { current } = inputRef; 29 | subscriptionRef.current = fromEvent(current, "keyup") 30 | .pipe(debounceTime(delay)) 31 | .subscribe((evt: any) => { 32 | const { value } = evt.target 33 | giveValue( 34 | typeof value === "string" ? 35 | value.trim() : 36 | value 37 | ); 38 | }); 39 | 40 | return () => { 41 | subscriptionRef.current?.unsubscribe() 42 | } 43 | }, [giveValue, delay]); 44 | 45 | return ( 46 | <> 47 | {component === "textarea" ? ( 48 | 52 | ) : ( 53 | 57 | ) 58 | } 59 | 60 | ); 61 | } 62 | 63 | export default memo(DelayInput); 64 | -------------------------------------------------------------------------------- /src/__fixtures__/history/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useRef, useState } from "react" 2 | import { FilterList, KeyboardArrowDown } from "@material-ui/icons" 3 | import Menu from "../menu" 4 | import ClickAwayListener from "@material-ui/core/ClickAwayListener" 5 | import ChartCard from "../chartCard" 6 | import { useTranslation } from "react-i18next" 7 | import { KeyOfStringInterface } from "../../constants" 8 | 9 | 10 | const fakes = [ 11 | { 12 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 13 | title: "This is the chart and do you like ?", 14 | timestamp: new Date().toString() 15 | }, 16 | { 17 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 18 | title: "This is the chart and do you like ?", 19 | timestamp: new Date().toString() 20 | }, { 21 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 22 | title: "This is the chart and do you like ?", 23 | timestamp: new Date().toString() 24 | }, { 25 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 26 | title: "This is the chart and do you like ?", 27 | timestamp: new Date().toString() 28 | }, { 29 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 30 | title: "This is the chart and do you like ?", 31 | timestamp: new Date().toString() 32 | }, { 33 | img: "https://docs.mongodb.com/charts/master/_images/stacked-bar-chart-reference-small.png", 34 | title: "This is the chart and do you like ?", 35 | timestamp: new Date().toString() 36 | } 37 | ] 38 | 39 | 40 | function History() { 41 | 42 | // references 43 | const dateFilterMenuRef = useRef() 44 | const chartFilterMenuRef = useRef() 45 | 46 | // trans 47 | const { t } = useTranslation() 48 | 49 | // component state 50 | const [state, setState] = useState({ 51 | dateFilter: "latest_first", 52 | chartFilter: "", 53 | 54 | curDateFilterDispl: "", 55 | curChartTypeFilterDispl: "" 56 | }) 57 | const { curChartTypeFilterDispl, curDateFilterDispl } = state 58 | 59 | const dateFilterMap: KeyOfStringInterface = { 60 | "collection.filterDate.olderFirst": "older_first", 61 | "collection.filterDate.latestFirst": "latest_first" 62 | } 63 | 64 | const chartTypeMap: KeyOfStringInterface = { 65 | "chartType.pie": "pie", 66 | "chartType.line": "line", 67 | "chartType.bar": "bar", 68 | "chartType.scatter": "scatter", 69 | "chartType.area": "area", 70 | } 71 | 72 | const menuFilters = [ 73 | { 74 | placeholder: t("collection.filterDate.placeholder"), 75 | curDisplay: curDateFilterDispl, 76 | menuList: Object.keys(dateFilterMap).map(item => { 77 | return { 78 | display: t(item), 79 | returnVal: item 80 | } 81 | }), 82 | ref: dateFilterMenuRef, 83 | stateKey: "dateFilter" 84 | }, 85 | { 86 | placeholder: t("collection.filterChartType.placeholder"), 87 | curDisplay: curChartTypeFilterDispl, 88 | menuList: Object.keys(chartTypeMap).map(item => { 89 | return { 90 | display: t(item), 91 | returnVal: item 92 | } 93 | }), 94 | ref: chartFilterMenuRef, 95 | stateKey: "chartFilter" 96 | } 97 | ] 98 | 99 | const toggleMenu = (ref: React.MutableRefObject, act: "close" | "open") => { 100 | (ref.current as HTMLElement).classList[act === "open" ? "remove" : "add"]("hidden") 101 | } 102 | 103 | const setFilterTypes = (stateKey: string) => (value: string) => { 104 | // stateKey is either "dateFilter" or "chartFilter" 105 | setState({ 106 | ...state, 107 | [stateKey]: dateFilterMap[value] || chartTypeMap[value], // stateKey is used as key here, so it must be exactly the same 108 | [stateKey === "dateFilter" ? "curDateFilterDispl" : "curChartTypeFilterDispl"]: value 109 | }) 110 | } 111 | 112 | return ( 113 |
114 |

115 | {t("collection.collection")} 116 |

117 | 118 | {/* filter */} 119 |
120 |
121 | 122 | {t("collection.filter")} 123 |
124 | {menuFilters.map((filter, idx) => ( 125 |
129 | toggleMenu(filter.ref, "close")} 131 | > 132 |
toggleMenu(filter.ref, "open")} 135 | > 136 | 137 | {t(filter.curDisplay) || filter.placeholder} 138 | 139 | 140 |
141 |
142 | 148 |
149 | ))} 150 |
151 | {/* charts */} 152 |
153 | {fakes.map((item, idx) => ( 154 | 160 | ))} 161 |
162 |
163 | ) 164 | } 165 | 166 | export default memo(History) 167 | -------------------------------------------------------------------------------- /src/__fixtures__/icons/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SvgIcon from '@material-ui/core/SvgIcon' 3 | 4 | 5 | export const AreaChart = (props: any) => ( 6 | 7 | 8 | 9 | ) 10 | 11 | export const ChartBar = (props: any) => ( 12 | 13 | 14 | 15 | ) 16 | 17 | export const Scatter = (props: any) => ( 18 | 19 | 20 | 21 | ) 22 | -------------------------------------------------------------------------------- /src/__fixtures__/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ApolloClient, InMemoryCache } from "@apollo/client" 3 | import { ApolloProvider, makeVar } from "@apollo/client" 4 | import { KeyOfStringInterface } from "../constants"; 5 | import { BarchartState } from "./chart/barChartInput/reducer"; 6 | import { LineChartState } from "./chart/lineChartInput/reducer"; 7 | import { PieChartState } from "./chart/pieChartInput/reducer"; 8 | import { InitAreaChartState, InitBarChartState, InitLineChartState, InitPieChartState } from "./initState"; 9 | import { BrowserRouter } from "react-router-dom" 10 | import Layout from "./layout" 11 | import { createUploadLink } from "apollo-upload-client" 12 | import { AreaChartState } from "./chart/areaChartInput/reducer"; 13 | 14 | 15 | export type ChartType = 16 | | "Bar chart" 17 | | "Pie chart" 18 | | "Line chart" 19 | | "Area chart" 20 | | "Scatter chart" 21 | 22 | export type localStateKey = 23 | | "chartDrawMutexReleased" 24 | | "chartType" 25 | | "barChartState" 26 | | "lineChartState" 27 | | "pieChartState" 28 | | "areaChartState" 29 | | "scatterChartState" 30 | | "isSignedIn" 31 | 32 | export interface LocalState extends KeyOfStringInterface { 33 | chartType: ChartType; 34 | chartDrawMutexReleased: boolean; 35 | barChartState: BarchartState; 36 | lineChartState: LineChartState; 37 | pieChartState: PieChartState; 38 | areaChartState: AreaChartState; 39 | scatterChartState: any; 40 | isSignedIn: boolean; 41 | } 42 | 43 | // local state 44 | export const localState = makeVar({ 45 | chartType: "Bar chart", 46 | chartDrawMutexReleased: true, 47 | barChartState: InitBarChartState, 48 | lineChartState: InitLineChartState, 49 | pieChartState: InitPieChartState, 50 | areaChartState: InitAreaChartState, 51 | scatterChartState: null, 52 | isSignedIn: true 53 | }) 54 | 55 | const apolloUploadLink = createUploadLink({ 56 | uri: "https://localhost:4000/graphql", 57 | // credentials: "include" 58 | }) 59 | 60 | const client = new ApolloClient({ 61 | link: apolloUploadLink, 62 | cache: new InMemoryCache({ 63 | typePolicies: { 64 | Query: { 65 | fields: {} 66 | } 67 | } 68 | }), 69 | credentials: "include" 70 | }) 71 | 72 | function App() { 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /src/__fixtures__/initState.ts: -------------------------------------------------------------------------------- 1 | import { defaultFieldColor } from "../constants"; 2 | import { AreaChartState } from "./chart/areaChartInput/reducer"; 3 | import { BarchartState } from "./chart/barChartInput/reducer"; 4 | import { LineChartState } from "./chart/lineChartInput/reducer"; 5 | import { PieChartState } from "./chart/pieChartInput/reducer"; 6 | 7 | 8 | export const InitBarChartState: BarchartState = { 9 | chartTitle: "", 10 | xLabel: "", 11 | yLabel: "", 12 | xData: [""], 13 | colors: [defaultFieldColor], 14 | yData: [ 15 | [ 16 | { 17 | name: "", 18 | value: "", 19 | error: undefined 20 | } 21 | ] 22 | ] 23 | } 24 | 25 | export const InitLineChartState: LineChartState = { 26 | chartTitle: "", 27 | xLabel: "", 28 | yLabel: "", 29 | xData: [""], 30 | yData: [ 31 | { 32 | name: "", 33 | data: [""], 34 | color: defaultFieldColor 35 | } 36 | ] 37 | } 38 | 39 | export const InitPieChartState: PieChartState = { 40 | chartTitle: "", 41 | pies: [ 42 | { 43 | slices: [ 44 | { 45 | name: "", 46 | value: "1", 47 | error: undefined, 48 | color: defaultFieldColor 49 | } 50 | ], 51 | name: "" 52 | } 53 | ] 54 | } 55 | 56 | export const InitAreaChartState: AreaChartState = { 57 | chartTitle: "", 58 | xData: [""], 59 | xLabel: "", 60 | yLabel: "", 61 | yData: [ 62 | { 63 | name: "", 64 | data: [""], 65 | color: defaultFieldColor 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/__fixtures__/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, Suspense } from "react" 2 | import Navigator from "../navigator" 3 | import { Route } from "react-router-dom" 4 | import { authRoute, routes, homeRoute } from "./routeConfig" 5 | import Gears from "../loading/Gears" 6 | import { Helmet } from "react-helmet" 7 | import dayjs from "dayjs" 8 | 9 | 10 | function Layout() { 11 | 12 | return ( 13 |
14 | 15 | 16 | Schart - Chart drawer for students 17 | 18 | 19 | 20 |
21 | {Object.values(routes).map((route, idx) => ( 22 | 27 | 28 | 29 | ))} 30 | 34 | 35 | 36 | 40 | 41 | 42 |
43 |
44 | © Schart {dayjs().year()} 45 |
46 |
47 | ) 48 | } 49 | 50 | export default memo(function () { 51 | return ( 52 | 54 | 55 | 56 | )}> 57 | 58 | 59 | ) 60 | }) 61 | 62 | -------------------------------------------------------------------------------- /src/__fixtures__/layout/routeConfig.tsx: -------------------------------------------------------------------------------- 1 | import Pages from "../pages" 2 | 3 | 4 | 5 | export const authRoute = { 6 | path: "/auth", 7 | component: Pages.AuthPage, 8 | } 9 | 10 | export const homeRoute = { 11 | path: "/", 12 | component: Pages.HomePage, 13 | } 14 | 15 | export const routes = { 16 | // user: { 17 | // path: "/username", 18 | // component: Pages.UserPage, 19 | // i18Name: "myProfile" 20 | // }, 21 | chart: { 22 | path: "/chart", 23 | component: Pages.ChartPage, 24 | i18Name: "chart" 25 | }, 26 | // weather: { 27 | // path: "/weather", 28 | // component: Pages.WeatherPage, 29 | // i18Name: "weather" 30 | // }, 31 | about: { 32 | path: "/about", 33 | component: Pages.AboutPage, 34 | i18Name: "about" 35 | }, 36 | qna: { 37 | path: "/qa", 38 | component: Pages.QaPage, 39 | i18Name: "qa" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/__fixtures__/loading/Gears.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react' 2 | 3 | 4 | const Gears = ({ width = 40, height = 40, ...other }) => { 5 | return ( 6 |
7 | 17 | 18 | 19 | 20 | 22 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | 37 | 38 |
39 | ) 40 | } 41 | 42 | export default memo(Gears) 43 | -------------------------------------------------------------------------------- /src/__fixtures__/logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | 3 | 4 | interface LogoProps { 5 | addClass?: string; 6 | } 7 | 8 | function Logo({ addClass }: LogoProps) { 9 | return ( 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default memo(Logo) 18 | -------------------------------------------------------------------------------- /src/__fixtures__/menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | 3 | 4 | export interface MenuProps { 5 | addClass?: string; 6 | values: { 7 | display: React.ReactNode, 8 | returnVal?: any; 9 | }[]; 10 | giveValue: (value: any) => void; 11 | refer: React.MutableRefObject; 12 | } 13 | 14 | function Menu({ addClass, values, giveValue, refer }: MenuProps) { 15 | return ( 16 |
17 | {values.map((value, idx) => ( 18 |
giveValue(value.returnVal || value.display)} 22 | > 23 | {value.display} 24 |
25 | ))} 26 |
27 | ) 28 | } 29 | 30 | export default memo(Menu) 31 | -------------------------------------------------------------------------------- /src/__fixtures__/navigator/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useMemo, useRef, useState } from "react" 2 | import { ArrowDropDown } from "@material-ui/icons" 3 | import ClickAwayListener from "@material-ui/core/ClickAwayListener" 4 | import Button from "@material-ui/core/Button" 5 | import Menu from "../menu" 6 | import Logo from "../logo" 7 | import { NavLink } from "react-router-dom" 8 | import { routes, authRoute } from "../layout/routeConfig" 9 | import "./style.css" 10 | import { localState } from ".." 11 | import { useTranslation } from "react-i18next" 12 | import { useReactiveVar } from "@apollo/client" 13 | 14 | 15 | type lang = "Tiếng Việt" | "English" | "中文" 16 | type LangMap = { 17 | [key in lang]: "vi" | "en" | "cn" 18 | } 19 | const langMap: LangMap = { 20 | "English": "en", 21 | "Tiếng Việt": "vi", 22 | "中文": "cn" 23 | } 24 | 25 | function Navigator() { 26 | 27 | // refs 28 | const userMenuRef = useRef() 29 | const langMenuRef = useRef() 30 | 31 | // translation 32 | const { t, i18n } = useTranslation() 33 | 34 | const [currentLang, setLang] = useState( 35 | localStorage.getItem("lang") as lang || "English" 36 | ) 37 | 38 | const { userMenuVales, langValues } = useMemo<{ 39 | userMenuVales: { 40 | display: any; 41 | }[]; 42 | langValues: { 43 | display: React.ReactNode; 44 | returnVal: lang | string; 45 | }[]; 46 | }>(() => { 47 | return { 48 | userMenuVales: [ 49 | { 50 | display: t("signout") 51 | }, 52 | { 53 | display: t("myProfile") 54 | } 55 | ], 56 | langValues: Object.keys(langMap).map(item => { 57 | return { 58 | display:

{item}

, 59 | returnVal: item 60 | } 61 | }) 62 | } 63 | }, [t]) 64 | 65 | 66 | // run everytime user change display language 67 | useEffect(() => { 68 | i18n.changeLanguage(langMap[currentLang]) 69 | 70 | localStorage.setItem("lang", currentLang) 71 | }, [currentLang, i18n]) 72 | 73 | // reactive variable to know whether user is authenticated or not 74 | const { isSignedIn } = useReactiveVar(localState) 75 | 76 | function toggleMenu(whichRef: React.MutableRefObject, type: "open" | "close") { 77 | (whichRef.current as HTMLElement).classList[type === "open" ? "remove" : "add"]("hidden") 78 | } 79 | 80 | const langImgSrc = (() => { 81 | let flagName; 82 | switch (currentLang) { 83 | case "English": flagName = "united-kingdom.svg"; break; 84 | case "Tiếng Việt": flagName = "vietnam.svg"; break; 85 | case "中文": flagName = "china.svg"; break; 86 | } 87 | return `/static/image/${flagName}` 88 | })() 89 | 90 | return ( 91 |
92 | 93 | 94 | 95 |
96 |
97 | {Object.values(routes).map((nav, idx) => { 98 | return ( 99 | 106 | {t(nav.i18Name)} 107 | 108 | ) 109 | })} 110 |
111 | {!isSignedIn ? ( 112 |
113 | toggleMenu(userMenuRef, "close")}> 114 |
toggleMenu(userMenuRef, "open")}> 115 |
116 | Phuc nguyen 117 |
118 |
119 | Nguyen Van Phuc 120 | 121 |
122 |
123 |
124 | 130 |
131 | ) : ( 132 | 137 | 145 | 146 | ) 147 | } 148 |
toggleMenu(langMenuRef, "open")}> 149 | toggleMenu(langMenuRef, "close")}> 150 |
153 | flag 154 |

{currentLang}

155 |
156 |
157 | 163 |
164 |
165 |
166 | ) 167 | } 168 | 169 | export default memo(Navigator) 170 | -------------------------------------------------------------------------------- /src/__fixtures__/navigator/style.css: -------------------------------------------------------------------------------- 1 | .text_blue_700 { 2 | color: #2b6cb0 !important; 3 | } -------------------------------------------------------------------------------- /src/__fixtures__/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react" 2 | import { Helmet } from "react-helmet" 3 | 4 | 5 | function AboutUs() { 6 | 7 | // memoized values 8 | const members = useMemo(() => { 9 | return [ 10 | { 11 | path: "/static/image/co.png", 12 | name: "Bùi Thái Hiển", 13 | career: "Phó hiệu truởng trương THPT Nguyễn Bỉnh Khiêm", 14 | role: "Giáo viên phụ trách hướng dẫn" 15 | }, 16 | { 17 | path: "/static/image/phuc.JPG", 18 | name: "Nguyễn Văn Phúc", 19 | career: "Học sinh lớp 11b4, truờng THPT Nguyễn Bỉnh Khiêm, Vĩnh Bảo, Hải Phòng", 20 | role: "Lên ý tuởng dự án, tham gia thiết kế và phát triển" 21 | }, 22 | { 23 | path: "/static/image/huyen.JPG", 24 | name: "Đỗ Thuý Huyền", 25 | career: "Học sinh lớp 10c1, truờng THPT Nguyễn Bỉnh Khiêm, Vĩnh Bảo, Hải Phòng", 26 | role: "Thành viên phát triển dự án" 27 | }, 28 | { 29 | path: "/static/image/ngoc.JPG", 30 | name: "Bùi Minh Ngọc", 31 | career: "Học sinh lớp 10c1, truờng THPT Nguyễn Bỉnh Khiêm, Vĩnh Bảo, Hải Phòng", 32 | role: "Thành viên phát triển dự án" 33 | }, 34 | // { 35 | // path: "/static/image/son.jpg", 36 | // name: "Lê Văn Sơn", 37 | // career: "Cựu học sinh truờng THPT Nguyễn Bỉnh Khiêm, Vĩnh Bảo, Hải Phòng", 38 | // role: "Thành viên phát triển dự án" 39 | // } 40 | ] 41 | }, []) 42 | 43 | return ( 44 |
45 | 46 | 47 | About Us 48 | 49 | 50 |
55 |
56 |

57 | Lời cảm ơn 58 |

59 |
60 |

61 | Dự án này, có lẽ sẽ không đến được với các thầy cô, các bạn học sinh của Tổ Quốc Việt Nam nếu như không có sự đóng góp của các thành viên dự án. Cảm ơn sự đóng góp của các bạn, cũng như sự ủng hộ của cộng đồng các bạn trẻ trên toàn thể lãnh thổ đất nước và cả hai mẹ con chị Janet Wang, bạn Tommy đến từ Trung Quốc về bản dịch tiếng Trung. Mong rằng Schart sẽ sẽ đem tới những niềm vui mới cho các bạn trong những tiết học địa lý tới đây. 62 |

63 |
64 |

65 | Nhóm phát triển 66 |

67 |
68 |
69 |
70 | Các thành viên dự án 71 |
72 |
73 | {members.map((item, idx) => { 74 | const info = [ 75 | { label: "Tên", val: item.name }, 76 | { label: "Nơi công tác", val: item.career }, 77 | { label: "Vai trò", val: item.role }, 78 | ] 79 | return ( 80 |
83 |
84 |
85 | member 86 |
87 |
88 | {info.map(inf => ( 89 |
90 | {inf.label} 91 | {inf.val} 92 |
93 | ))} 94 |
95 |
96 |
97 | ) 98 | })} 99 |
100 |
101 | ) 102 | } 103 | 104 | export default memo(AboutUs) 105 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/Chart.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | import ChartComponent from "../chart" 3 | import { Helmet } from "react-helmet" 4 | 5 | 6 | function ChartDraw() { 7 | return ( 8 | <> 9 | 10 | Draw chart 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default memo(ChartDraw) 18 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | import { Redirect } from "react-router-dom" 3 | 4 | 5 | function Home() { 6 | return ( 7 | 10 | ) 11 | } 12 | 13 | export default memo(Home) 14 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/QA.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | import { Helmet } from "react-helmet" 3 | 4 | 5 | function Qa() { 6 | return ( 7 |
8 | 9 | 10 | Q & A 11 | 12 | 13 |
14 | weather page 15 |
16 |
17 | ) 18 | } 19 | 20 | export default memo(Qa) 21 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/User.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | import UserInfo from "../userInfo" 3 | import ChartHistory from "../history" 4 | import { Helmet } from "react-helmet" 5 | 6 | 7 | function UserPage() { 8 | return ( 9 |
10 | 11 | 12 | My profile 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default memo(UserPage) 26 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/Weather.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react" 2 | import { Helmet } from "react-helmet" 3 | 4 | 5 | function Weather() { 6 | return ( 7 |
8 | 9 | 10 | Weather 11 | 12 | 13 |
14 | weather page 15 |
16 |
17 | ) 18 | } 19 | 20 | export default memo(Weather) 21 | -------------------------------------------------------------------------------- /src/__fixtures__/pages/index.ts: -------------------------------------------------------------------------------- 1 | import ChartPage from "./Chart" 2 | import UserPage from "./User" 3 | import AuthPage from "../auth" 4 | import WeatherPage from "./Weather" 5 | import AboutPage from "./About" 6 | import QaPage from "./QA" 7 | import HomePage from "./Home" 8 | 9 | 10 | export default { 11 | ChartPage, 12 | UserPage, 13 | AuthPage, 14 | WeatherPage, 15 | AboutPage, 16 | QaPage, 17 | HomePage 18 | } 19 | -------------------------------------------------------------------------------- /src/__fixtures__/userInfo/croppie.css: -------------------------------------------------------------------------------- 1 | .croppie-container { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .croppie-container .cr-image { 7 | z-index: -1; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | transform-origin: 0 0; 12 | max-height: none; 13 | max-width: none; 14 | } 15 | 16 | .croppie-container .cr-boundary { 17 | position: relative; 18 | overflow: hidden; 19 | margin: 0 auto; 20 | z-index: 1; 21 | width: 100%; 22 | height: 100%; 23 | border-radius: 5px; 24 | } 25 | 26 | .croppie-container .cr-viewport, 27 | .croppie-container .cr-resizer { 28 | position: absolute; 29 | border: 2px solid #fff; 30 | margin: auto; 31 | top: 0; 32 | bottom: 0; 33 | right: 0; 34 | left: 0; 35 | box-shadow: 0 0 2000px 2000px rgba(0, 0, 0, 0.5); 36 | z-index: 0; 37 | } 38 | 39 | .croppie-container .cr-resizer { 40 | z-index: 2; 41 | box-shadow: none; 42 | pointer-events: none; 43 | } 44 | 45 | .croppie-container .cr-resizer-vertical, 46 | .croppie-container .cr-resizer-horisontal { 47 | position: absolute; 48 | pointer-events: all; 49 | } 50 | 51 | .croppie-container .cr-resizer-vertical::after, 52 | .croppie-container .cr-resizer-horisontal::after { 53 | display: block; 54 | position: absolute; 55 | box-sizing: border-box; 56 | border: 1px solid black; 57 | background: #fff; 58 | width: 10px; 59 | height: 10px; 60 | content: ''; 61 | } 62 | 63 | .croppie-container .cr-resizer-vertical { 64 | bottom: -5px; 65 | cursor: row-resize; 66 | width: 100%; 67 | height: 10px; 68 | } 69 | 70 | .croppie-container .cr-resizer-vertical::after { 71 | left: 50%; 72 | margin-left: -5px; 73 | } 74 | 75 | .croppie-container .cr-resizer-horisontal { 76 | right: -5px; 77 | cursor: col-resize; 78 | width: 10px; 79 | height: 100%; 80 | } 81 | 82 | .croppie-container .cr-resizer-horisontal::after { 83 | top: 50%; 84 | margin-top: -5px; 85 | } 86 | 87 | .croppie-container .cr-original-image { 88 | display: none; 89 | } 90 | 91 | .croppie-container .cr-vp-circle { 92 | border-radius: 50%; 93 | } 94 | 95 | .croppie-container .cr-overlay { 96 | z-index: 1; 97 | position: absolute; 98 | cursor: move; 99 | touch-action: none; 100 | } 101 | 102 | .croppie-container .cr-slider-wrap { 103 | width: 75%; 104 | margin: 15px auto; 105 | text-align: center; 106 | } 107 | 108 | .croppie-result { 109 | position: relative; 110 | overflow: hidden; 111 | } 112 | 113 | .croppie-result img { 114 | position: absolute; 115 | } 116 | 117 | .croppie-container .cr-image, 118 | .croppie-container .cr-overlay, 119 | .croppie-container .cr-viewport { 120 | -webkit-transform: translateZ(0); 121 | -moz-transform: translateZ(0); 122 | -ms-transform: translateZ(0); 123 | transform: translateZ(0); 124 | } 125 | 126 | .cr-slider { 127 | -webkit-appearance: none; 128 | max-width: 100%; 129 | background-color: transparent; 130 | } 131 | 132 | .cr-slider::-webkit-slider-runnable-track { 133 | width: 100%; 134 | height: 2px; 135 | background: #a7a7a7; 136 | border: 0; 137 | border-radius: 1px; 138 | } 139 | 140 | .cr-slider::-webkit-slider-thumb { 141 | -webkit-appearance: none; 142 | border: 2px solid #4f89ff; 143 | height: 16px; 144 | width: 16px; 145 | border-radius: 50%; 146 | background: #fff; 147 | margin-top: -7px; 148 | } 149 | 150 | .cr-slider:focus { 151 | outline: none; 152 | } 153 | 154 | .cr-slider::-moz-range-track { 155 | width: 100%; 156 | height: 3px; 157 | background: rgba(0, 0, 0, 0.5); 158 | border: 0; 159 | border-radius: 3px; 160 | } 161 | 162 | .cr-slider::-moz-range-thumb { 163 | border: none; 164 | height: 16px; 165 | width: 16px; 166 | border-radius: 50%; 167 | background: #ddd; 168 | margin-top: -6px; 169 | } 170 | 171 | /*hide the outline behind the border*/ 172 | .cr-slider:-moz-focusring { 173 | outline: 1px solid white; 174 | outline-offset: -1px; 175 | } 176 | 177 | .cr-slider::-ms-track { 178 | width: 100%; 179 | height: 5px; 180 | background: transparent; 181 | /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ 182 | border-color: transparent;/*leave room for the larger thumb to overflow with a transparent border */ 183 | border-width: 6px 0; 184 | color: transparent;/*remove default tick marks*/ 185 | } 186 | .cr-slider::-ms-fill-lower { 187 | background: rgba(0, 0, 0, 0.5); 188 | border-radius: 10px; 189 | } 190 | .cr-slider::-ms-fill-upper { 191 | background: rgba(0, 0, 0, 0.5); 192 | border-radius: 10px; 193 | } 194 | .cr-slider::-ms-thumb { 195 | border: none; 196 | height: 16px; 197 | width: 16px; 198 | border-radius: 50%; 199 | background: #ddd; 200 | margin-top:1px; 201 | } 202 | .cr-slider:focus::-ms-fill-lower { 203 | background: rgba(0, 0, 0, 0.5); 204 | } 205 | .cr-slider:focus::-ms-fill-upper { 206 | background: rgba(0, 0, 0, 0.5); 207 | } 208 | /*******************************************/ 209 | 210 | /***********************************/ 211 | /* Rotation Tools */ 212 | /***********************************/ 213 | .cr-rotate-controls { 214 | position: absolute; 215 | bottom: 5px; 216 | left: 5px; 217 | z-index: 1; 218 | } 219 | .cr-rotate-controls button { 220 | border: 0; 221 | background: none; 222 | } 223 | .cr-rotate-controls i:before { 224 | display: inline-block; 225 | font-style: normal; 226 | font-weight: 900; 227 | font-size: 22px; 228 | } 229 | .cr-rotate-l i:before { 230 | content: '↺'; 231 | } 232 | .cr-rotate-r i:before { 233 | content: '↻'; 234 | } 235 | -------------------------------------------------------------------------------- /src/__fixtures__/userInfo/editor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState, memo } from "react" 2 | import DialogContent from "@material-ui/core/DialogContent" 3 | import DialogTitle from "@material-ui/core/DialogTitle" 4 | import DialogActions from "@material-ui/core/DialogActions" 5 | import IconButton from "@material-ui/core/IconButton" 6 | import Button from "@material-ui/core/Button" 7 | import { CameraAlt } from "@material-ui/icons" 8 | import Croppie from "croppie" 9 | import "./croppie.css" 10 | import { useTranslation } from "react-i18next" 11 | import { useMutation } from "@apollo/client" 12 | import { UPLOAD_RESIZE_IMAGE } from "../../graphql/mutations" 13 | 14 | 15 | export interface AvatarDialogProps { 16 | onClose: () => void; 17 | } 18 | 19 | function AvatarDialog({ onClose }: AvatarDialogProps) { 20 | 21 | // trans 22 | const { t } = useTranslation() 23 | 24 | // references 25 | const crpRef = useRef() 26 | 27 | // croppie options 28 | const croppieOps: Croppie.CroppieOptions = { 29 | boundary: { width: 300, height: 280 }, 30 | enableZoom: true, 31 | viewport: { width: 250, height: 250, type: "square" } 32 | } 33 | 34 | // component state 35 | const [state, setState] = useState<{ 36 | croppie: Croppie | null; 37 | canSave: boolean; 38 | file: File | null; 39 | }>({ 40 | croppie: null, 41 | canSave: false, 42 | file: null 43 | }) 44 | const { croppie, canSave, file } = state 45 | 46 | useEffect(() => { 47 | if (!state.croppie) { 48 | setState({ 49 | ...state, 50 | croppie: new Croppie( 51 | crpRef.current as HTMLDivElement, 52 | croppieOps 53 | ) 54 | }) 55 | } 56 | }, [croppieOps, state]) 57 | 58 | const handleSelectImg = (evt: React.ChangeEvent) => { 59 | const { files } = evt.target 60 | if (files && files[0]) { 61 | // binding croppie to new image source, after user choosing new image from their device. 62 | croppie?.bind({ 63 | url: (URL || webkitURL).createObjectURL(files[0]), 64 | zoom: 2, 65 | }) 66 | setState({ 67 | ...state, 68 | croppie, 69 | canSave: true, 70 | file: files[0] 71 | }) 72 | } 73 | } 74 | 75 | // mutation 76 | const [ 77 | upFile, 78 | // { data, loading } 79 | ] = useMutation(UPLOAD_RESIZE_IMAGE) 80 | 81 | const handleSave = () => { 82 | upFile({ 83 | variables: { 84 | input: { 85 | file: file, 86 | dimenParam: croppie?.get().points, 87 | } 88 | } 89 | }) 90 | } 91 | 92 | return ( 93 | <> 94 | 95 | 102 | 108 | 109 | 110 |
111 |
112 | 113 | 121 | 128 | 129 | 130 | ) 131 | } 132 | 133 | export default memo(AvatarDialog) -------------------------------------------------------------------------------- /src/__fixtures__/userInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, memo, Suspense, useState } from "react" 2 | import { Button, Tooltip } from "@material-ui/core" 3 | import { Camera } from "@material-ui/icons" 4 | import DelayInput from "../delayinput" 5 | import Dialog from "@material-ui/core/Dialog" 6 | import Slide from "@material-ui/core/Slide" 7 | import { TransitionProps } from "@material-ui/core/transitions/transition" 8 | import useMediaQuery from '@material-ui/core/useMediaQuery'; 9 | import { useTheme } from "@material-ui/core/styles" 10 | import { useTranslation } from "react-i18next" 11 | 12 | 13 | const AvatarDialog = lazy(() => import("./editor")) 14 | 15 | 16 | const Transition = React.forwardRef(function ( 17 | props: TransitionProps & { children?: React.ReactElement }, 18 | ref: React.Ref, 19 | ) { 20 | return ; 21 | }); 22 | 23 | 24 | function UserInfo() { 25 | 26 | const theme = useTheme() 27 | const fullScreen = useMediaQuery(theme.breakpoints.down("xs")) 28 | 29 | // trans 30 | const { t } = useTranslation() 31 | 32 | // component state 33 | const [state, setState] = useState({ 34 | name: "Le Minh Son", 35 | email: "leminhson2398@outlook.com", 36 | geoThought: "Well i really like learning Geography Especially in the lessons that teachers require us to write graphs", 37 | edit: false, 38 | open: false 39 | }) 40 | const { name, email, geoThought, edit, open } = state 41 | 42 | const values: { 43 | name: "email" | "name"; 44 | value: string; 45 | }[] = [ 46 | { name: "name", value: name }, 47 | { name: "email", value: email }, 48 | ] 49 | 50 | const editClickHandler = () => { 51 | setState({ 52 | ...state, 53 | edit: true 54 | }) 55 | } 56 | 57 | return ( 58 |
59 |

60 | {t("aboutUser.about")} 61 |

62 | 63 | {/* avatar */} 64 |
65 | profile 66 |
setState({ 69 | ...state, 70 | open: true 71 | })} 72 | > 73 | 74 | 75 | 76 |
77 |
78 | 79 | {values.map((item, idx) => ( 80 |
84 |
85 | {t(`aboutUser.${item.name}`)} 86 |
87 |
88 | {!edit ? ( 89 | <>{item.value} 90 | ) : ( 91 | { 97 | setState({ 98 | ...state, 99 | [item.name]: value.trim() 100 | }) 101 | }} 102 | /> 103 | )} 104 |
105 |
106 | ))} 107 | 108 |
109 | {t("aboutUser.thoughtAboutGeo")} 110 |
111 | {edit ? ( 112 | { 117 | setState({ 118 | ...state, 119 | geoThought: value.trim() 120 | }) 121 | }} 122 | /> 123 | ) : ( 124 |

125 | 126 | 127 | 128 | 129 | 130 | {geoThought} 131 |

132 | )} 133 | 134 | {edit ? ( 135 |
136 | 139 | 142 |
143 | ) : ( 144 | 153 | ) 154 | } 155 | 163 | Loading...

165 | )}> 166 | setState({ 168 | ...state, 169 | open: false 170 | })} 171 | /> 172 |
173 |
174 |
175 | ) 176 | } 177 | 178 | export default memo(UserInfo) 179 | -------------------------------------------------------------------------------- /src/__fixtures__/utils/index.ts: -------------------------------------------------------------------------------- 1 | // import { BarchartState } from "../barChartInput/reducer" 2 | import { 3 | localState 4 | } from "../index" 5 | import { localStateKey } from "../index" 6 | // import { LineChartState } from "../lineChartInput/reducer" 7 | // import { PieChartState } from "../pieChartInput/reducer" 8 | 9 | 10 | export function noAnyError(errorList: any[]): boolean { 11 | return errorList.every(item => !item) 12 | } 13 | 14 | export function updateLocalState(key: localStateKey, value: any) { // objValue looks like this: { chartType: "Bar Chart" }, or similar 15 | const prevState = localState() 16 | localState({ 17 | ...prevState, 18 | [key]: value 19 | }) 20 | } 21 | 22 | export function fileGenerator(data: string, filename: string, type: string) { 23 | var file = new Blob([data], { type }); 24 | if (window.navigator.msSaveOrOpenBlob) // IE10+ 25 | window.navigator.msSaveOrOpenBlob(file, filename); 26 | else { // Others 27 | var a = document.createElement("a"), 28 | url = (URL || webkitURL).createObjectURL(file); 29 | a.href = url; 30 | a.download = filename; 31 | document.body.appendChild(a); 32 | a.click(); 33 | setTimeout(function () { 34 | document.body.removeChild(a); 35 | (URL || webkitURL).revokeObjectURL(url); 36 | }, 0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { ChartType } from "../__fixtures__" 2 | import React from "react" 3 | 4 | 5 | export const 6 | MAX_CHART_INPUT_HEIGHT = 550, 7 | isRealNumber = /^-?\d*\.?\d*$/ // this accepts real numbers 8 | 9 | export interface KeyOfStringInterface { 10 | [key: string]: any 11 | } 12 | 13 | export type KeyOfChartType = { 14 | [key in ChartType]: React.MemoExoticComponent<() => JSX.Element> 15 | } 16 | 17 | export const defaultColors = [ 18 | "rgb(244, 78, 59)", 19 | "rgb(254, 146, 0)", 20 | "rgb(104, 188, 0)", 21 | "rgb(250, 40, 255)", 22 | "rgb(128, 137, 0)", 23 | "rgb(196, 81, 0)", 24 | "rgb(0, 156, 224)" 25 | ], defaultFieldColor = defaultColors[0] 26 | -------------------------------------------------------------------------------- /src/graphql/mutations.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client" 2 | 3 | 4 | export const UPLOAD_RESIZE_IMAGE = gql` 5 | mutation UploadFile($input: UploadFileInput!) { 6 | uploadFile(input: $input) { 7 | ok 8 | } 9 | } 10 | ` 11 | -------------------------------------------------------------------------------- /src/graphql/queries.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client" 2 | 3 | 4 | export const GET_CURRENT_CHART_STATE = gql` 5 | query CurrentChartState { 6 | currentChartState @client 7 | } 8 | `; 9 | 10 | // export const LOGIN = gql` 11 | // query login() 12 | // ` 13 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | 4 | import Backend from 'i18next-http-backend'; 5 | import LanguageDetector from 'i18next-browser-languagedetector'; 6 | // don't want to use this? 7 | // have a look at the Quick start guide 8 | // for passing in lng and translations on init 9 | 10 | i18n 11 | // load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales) 12 | // learn more: https://github.com/i18next/i18next-http-backend 13 | .use(Backend) 14 | // detect user language 15 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 16 | .use(LanguageDetector) 17 | // pass the i18n instance to react-i18next. 18 | .use(initReactI18next) 19 | // init i18next 20 | // for all options read: https://www.i18next.com/overview/configuration-options 21 | .init({ 22 | fallbackLng: 'en', 23 | // debug: true, 24 | 25 | interpolation: { 26 | escapeValue: false, // not needed for react as it escapes by default 27 | } 28 | }); 29 | 30 | 31 | export default i18n; -------------------------------------------------------------------------------- /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 | * { 11 | outline: none !important; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | 19 | .chart_nav_btn { 20 | top: 100%; 21 | left: 50%; 22 | border-right-color: transparent !important; 23 | border-bottom-color: transparent !important; 24 | border-left-color: transparent !important; 25 | } 26 | 27 | /* for animation */ 28 | .transiFade-enter { 29 | opacity: 0; 30 | } 31 | 32 | .transiFade-enter-active { 33 | opacity: 1; 34 | transition: opacity 0.6s ease-in; 35 | } 36 | 37 | .transiFade-exit { 38 | opacity: 1; 39 | } 40 | 41 | .transiFade-exit-active { 42 | opacity: 0; 43 | transition: opacity 0.6s ease-out; 44 | } 45 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import "./tailwind/out.css" 6 | import App from "./__fixtures__" 7 | 8 | import "./i18n" 9 | 10 | ReactDOM.render( 11 | <> 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.register(); 21 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 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/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/tailwind/in.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [ 3 | './src/**/*.html', 4 | './src/**/*.tsx', 5 | ], 6 | theme: { 7 | extend: {}, 8 | screens: { 9 | 'xl': { 'max': '1920px' }, 10 | // => @media (max-width: 1920px) { ... } 11 | 'lg': { 'max': '1280px' }, 12 | // => @media (max-width: 1280px) { ... } 13 | 'md': { 'max': '960px' }, 14 | // => @media (max-width: 960px) { ... } 15 | 'sm': { 'max': '600px' }, 16 | // => @media (max-width: 600px) { ... } 17 | 'xs': { 'max': '414px' } 18 | }, 19 | }, 20 | variants: { 21 | textColor: ['hover', 'group-hover'], 22 | opacity: ['hover', 'group-hover'], 23 | transform: ['group-hover'], 24 | translate: ['group-hover'], 25 | borderWidth: ['responsive', 'hover'], 26 | backgroundOpacity: ['responsive', 'hover', 'focus', 'group-hover'], 27 | display: ['responsive', 'group-hover'], 28 | }, 29 | plugins: [], 30 | corePlugins: { 31 | // outline: false, 32 | }, 33 | future: { 34 | removeDeprecatedGapUtilities: true, 35 | purgeLayersByDefault: true, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "suppressImplicitAnyIndexErrors": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------