add recipe viewer
This commit is contained in:
parent
92b11f7b9d
commit
f7f1535695
31 changed files with 1532 additions and 151 deletions
219
client-electron/package-lock.json
generated
219
client-electron/package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@postman/node-keytar": "^7.9.3",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
|
|
@ -18,11 +19,14 @@
|
|||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/match-sorter-utils": "^8.11.8",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
|
|
@ -48,12 +52,14 @@
|
|||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.334.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"sonner": "^1.4.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
|
|
@ -3128,6 +3134,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz",
|
||||
"integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-checkbox": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz",
|
||||
|
|
@ -3660,6 +3692,37 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
|
||||
"integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.1",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-direction": "1.0.1",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
||||
|
|
@ -3744,6 +3807,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
|
||||
"integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"@radix-ui/react-use-previous": "1.0.1",
|
||||
"@radix-ui/react-use-size": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||
|
|
@ -3808,6 +3900,40 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
||||
"integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-popper": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"@radix-ui/react-visually-hidden": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
|
|
@ -9806,6 +9932,14 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.334.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.334.0.tgz",
|
||||
"integrity": "sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-fetch-happen": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
|
||||
|
|
@ -11557,6 +11691,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.9.tgz",
|
||||
"integrity": "sha512-ZylBvs7oG7Y/INWw3oYGolqgpFvoPW8MPeg9l1fURDeKpxrmUuCHBUmPj47BdZ11MODImu3kZYXG85rbySab7w==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz",
|
||||
|
|
@ -15824,6 +15967,18 @@
|
|||
"@radix-ui/react-primitive": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-avatar": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz",
|
||||
"integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-checkbox": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz",
|
||||
|
|
@ -16099,6 +16254,23 @@
|
|||
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-scroll-area": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz",
|
||||
"integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/number": "1.0.1",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-direction": "1.0.1",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-select": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
|
||||
|
|
@ -16146,6 +16318,21 @@
|
|||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-switch": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
|
||||
"integrity": "sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"@radix-ui/react-use-previous": "1.0.1",
|
||||
"@radix-ui/react-use-size": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-tabs": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||
|
|
@ -16182,6 +16369,26 @@
|
|||
"@radix-ui/react-visually-hidden": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-tooltip": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
||||
"integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-popper": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"@radix-ui/react-visually-hidden": "1.0.3"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
|
|
@ -20624,6 +20831,12 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"lucide-react": {
|
||||
"version": "0.334.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.334.0.tgz",
|
||||
"integrity": "sha512-y0Rv/Xx6qAq4FutZ3L/efl3O9vl6NC/1p0YOg6mBfRbQ4k1JCE2rz0rnV7WC8Moxq1RY99vLATvjcqUegGJTvA==",
|
||||
"requires": {}
|
||||
},
|
||||
"make-fetch-happen": {
|
||||
"version": "10.2.1",
|
||||
"resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz",
|
||||
|
|
@ -21848,6 +22061,12 @@
|
|||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"react-resizable-panels": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.9.tgz",
|
||||
"integrity": "sha512-ZylBvs7oG7Y/INWw3oYGolqgpFvoPW8MPeg9l1fURDeKpxrmUuCHBUmPj47BdZ11MODImu3kZYXG85rbySab7w==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@postman/node-keytar": "^7.9.3",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
|
|
@ -30,11 +31,14 @@
|
|||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/match-sorter-utils": "^8.11.8",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
|
|
@ -60,12 +64,14 @@
|
|||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.334.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.49.3",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"sonner": "^1.4.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
|
|
|
|||
|
|
@ -1,26 +1,19 @@
|
|||
import { createBrowserRouter, createHashRouter, RouterProvider, type RouteObject } from 'react-router-dom'
|
||||
import { createBrowserRouter, createHashRouter, RouterProvider, type RouteObject, Navigate } from 'react-router-dom'
|
||||
import AuthCallBack from './AuthCallBack'
|
||||
import MainLayout from './layouts/MainLayout'
|
||||
import HomePage from './pages/home'
|
||||
import LoginPage from './pages/login'
|
||||
import AndroidPage from './pages/android/android'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import UploadPage from './pages/upload'
|
||||
import { type MenuList } from './components/sidebar'
|
||||
import { DashboardIcon, FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
|
||||
import SelectCountryPage from './pages/recipes/select-country'
|
||||
import { FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
|
||||
import RecipesTablePage from './pages/recipes/recipe-table'
|
||||
|
||||
const sideBarMenuList: MenuList = [
|
||||
{
|
||||
title: 'Home',
|
||||
icon: DashboardIcon,
|
||||
link: '/'
|
||||
},
|
||||
{
|
||||
title: 'Recipes',
|
||||
icon: FileTextIcon,
|
||||
link: '/recipes/select-country'
|
||||
link: '/recipes'
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
|
@ -41,15 +34,11 @@ function router() {
|
|||
element: <MainLayout sidebarMenu={sideBarMenuList} />,
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: <HomePage />
|
||||
path: '',
|
||||
element: <Navigate to="/recipes" />
|
||||
},
|
||||
{
|
||||
path: 'recipes/select-country',
|
||||
element: <SelectCountryPage />
|
||||
},
|
||||
{
|
||||
path: 'recipes/:country_id/:filename',
|
||||
path: 'recipes',
|
||||
element: <RecipesTablePage />
|
||||
},
|
||||
{
|
||||
|
|
|
|||
16
client-electron/src/assets/tao_logo.svg
Normal file
16
client-electron/src/assets/tao_logo.svg
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="136" height="102" viewBox="0 0 136 102" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.4261 38.3364V65.1097L12.4745 64.2196L51.5736 50.3544L39.4004 35.8294H14.3981L12.4261 38.3364Z" fill="#513C2F"/>
|
||||
<path d="M55.2988 49.0333L68.1106 44.4899L80.4911 48.958L92.8475 34.2L79.9953 17.86H55.7355L42.8833 34.2L55.2988 49.0333Z" fill="#513C2F"/>
|
||||
<path d="M84.2021 50.2973L122.827 64.2368V37.7289L121.333 35.8294H96.3304L84.2021 50.2973Z" fill="#513C2F"/>
|
||||
<path d="M122.306 31.3333C119.887 16.9571 109.272 5.34644 95.4069 1.42597C95.5207 1.45815 95.6343 1.49091 95.7477 1.52414L83.8292 16.677L96.3304 32.5707H121.333L122.306 31.3333Z" fill="#513C2F"/>
|
||||
<path d="M91.3327 0.514628C89.2959 0.176112 87.2039 0 85.0707 0H50.1821C47.9895 0 45.8406 0.186103 43.7502 0.543322C43.8166 0.531938 43.8831 0.520785 43.9496 0.509749L55.2144 14.8314H80.0718L91.3327 0.514628Z" fill="#513C2F"/>
|
||||
<path d="M39.7766 1.44572C31.3983 3.83206 24.2116 9.02795 19.3103 15.9444L19.117 16.1782L19.1464 16.1777C16.1371 20.493 14.0124 25.466 13.0338 30.8363L14.3981 32.5707H39.4004L51.8292 16.7691L39.7766 1.44572Z" fill="#513C2F"/>
|
||||
<path d="M83.0358 85.7588C85.6669 83.2025 87.6539 79.9968 88.7326 76.4033H77.0626L67.6264 69.3867L58.1902 76.4033H46.5203C47.5989 79.9968 49.5859 83.2025 52.2171 85.7588H83.0358Z" fill="#513C2F"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M89.4214 73.2848C89.5683 72.2663 89.6443 71.2251 89.6443 70.1663C89.6443 58.1103 79.7865 48.3368 67.6264 48.3368C55.4663 48.3368 45.6085 58.1103 45.6085 70.1663C45.6085 71.2251 45.6846 72.2663 45.8315 73.2848H56.6174L67.6264 65.4885L78.6354 73.2848H89.4214ZM78.6354 67.0478C80.3725 67.0478 81.7807 65.6516 81.7807 63.9293C81.7807 62.2071 80.3725 60.8108 78.6354 60.8108C76.8982 60.8108 75.49 62.2071 75.49 63.9293C75.49 65.6516 76.8982 67.0478 78.6354 67.0478ZM59.7628 63.9293C59.7628 65.6516 58.3546 67.0478 56.6174 67.0478C54.8803 67.0478 53.4721 65.6516 53.4721 63.9293C53.4721 62.2071 54.8803 60.8108 56.6174 60.8108C58.3546 60.8108 59.7628 62.2071 59.7628 63.9293Z" fill="#513C2F"/>
|
||||
<path d="M88.1113 101.351L100.693 88.8774H69.1992V101.351H88.1113Z" fill="#513C2F"/>
|
||||
<path d="M66.0537 88.8774H35.2509L47.8326 101.351H66.0537V88.8774Z" fill="#513C2F"/>
|
||||
<path d="M92.5596 101.351H104.526C109.061 101.351 113.375 99.4104 116.362 96.0266L122.671 88.8774H105.141L92.5596 101.351Z" fill="#513C2F"/>
|
||||
<path d="M30.8027 88.8774H12.5817L19.0344 96.0986C22.0193 99.439 26.3049 101.351 30.8062 101.351H43.3844L30.8027 88.8774Z" fill="#513C2F"/>
|
||||
<path d="M31.4541 66.4402H3.14537C2.2768 66.4402 1.57274 67.1383 1.57274 67.9995C1.57274 68.8606 2.2768 69.5587 3.14537 69.5587H6.29085V71.118H3.14537C1.40822 71.118 0 72.1651 0 73.4569C0 74.7486 1.40822 75.7957 3.14537 75.7957H6.29085V77.355H3.14537C1.40822 77.355 0 78.4021 0 79.6939C0 80.9856 1.40822 82.0327 3.14537 82.0327H6.29085V83.592H3.14537C2.2768 83.592 1.57274 84.2901 1.57274 85.1512C1.57274 86.0124 2.2768 86.7105 3.14537 86.7105H31.4541V86.681C31.593 86.6914 31.7327 86.6991 31.8731 86.7041C31.9951 86.7083 32.1175 86.7105 32.2405 86.7105C37.8863 86.7105 42.4631 82.1728 42.4631 76.5754C42.4631 70.9779 37.8863 66.4402 32.2405 66.4402C32.1849 66.4402 32.1295 66.4406 32.0742 66.4415C32.025 66.4423 31.9758 66.4435 31.9268 66.4449L31.86 66.4471L31.8096 66.4491C31.7471 66.4516 31.6849 66.4548 31.6229 66.4584C31.5665 66.4618 31.5103 66.4655 31.4541 66.4698V66.4402Z" fill="#513C2F"/>
|
||||
<path d="M132.107 87.3181H103.799V87.2885C103.539 87.3081 103.277 87.3181 103.012 87.3181C97.3665 87.3181 92.7897 82.7804 92.7897 77.1829C92.7897 71.5854 97.3665 67.0478 103.012 67.0478C103.277 67.0478 103.539 67.0578 103.799 67.0773V67.0478H132.107C132.976 67.0478 133.68 67.7459 133.68 68.607C133.68 69.4682 132.976 70.1663 132.107 70.1663H128.962V71.7255H132.107C133.845 71.7255 135.253 72.7727 135.253 74.0644C135.253 75.3562 133.845 76.4033 132.107 76.4033H128.962V77.9626H132.107C133.845 77.9626 135.253 79.0097 135.253 80.3014C135.253 81.5932 133.845 82.6403 132.107 82.6403H128.962V84.1996H132.107C132.976 84.1996 133.68 84.8977 133.68 85.7588C133.68 86.62 132.976 87.3181 132.107 87.3181Z" fill="#513C2F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { Link } from 'react-router-dom'
|
||||
import userAuthStore from '@/hooks/userAuth'
|
||||
import { Button } from './ui/button'
|
||||
import Logo from '@/assets/vite.svg'
|
||||
import Logo from '@/assets/tao_logo.svg'
|
||||
|
||||
const DropdownMenuUser: React.FC = () => {
|
||||
return (
|
||||
|
|
|
|||
48
client-electron/src/components/ui/avatar.tsx
Normal file
48
client-electron/src/components/ui/avatar.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
43
client-electron/src/components/ui/resizable.tsx
Normal file
43
client-electron/src/components/ui/resizable.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { DragHandleDots2Icon } from "@radix-ui/react-icons"
|
||||
import * as ResizablePrimitive from "react-resizable-panels"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<DragHandleDots2Icon className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
)
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
|
||||
46
client-electron/src/components/ui/scroll-area.tsx
Normal file
46
client-electron/src/components/ui/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
27
client-electron/src/components/ui/switch.tsx
Normal file
27
client-electron/src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
24
client-electron/src/components/ui/textarea.tsx
Normal file
24
client-electron/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
28
client-electron/src/components/ui/tooltip.tsx
Normal file
28
client-electron/src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
20
client-electron/src/hooks/android-switcher.ts
Normal file
20
client-electron/src/hooks/android-switcher.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { create } from 'zustand'
|
||||
import useAdb from './useAdb'
|
||||
|
||||
interface AndroidSwitcherHook {
|
||||
selectedAndroid: string
|
||||
setSelectedAndroid: (android: string) => void
|
||||
}
|
||||
|
||||
const useAndroidSwitcher = create<AndroidSwitcherHook>(set => ({
|
||||
selectedAndroid: '',
|
||||
setSelectedAndroid: android => set({ selectedAndroid: android })
|
||||
}))
|
||||
|
||||
useAdb.subscribe((state, prevState) => {
|
||||
if (state.device?.serial !== prevState.device?.serial) {
|
||||
useAndroidSwitcher.setState({ selectedAndroid: state.device?.serial ?? '' })
|
||||
}
|
||||
})
|
||||
|
||||
export default useAndroidSwitcher
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
import axios, { type AxiosInstance } from 'axios'
|
||||
import taoAxios from '@/lib/taoAxios'
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface AxiosHook {
|
||||
axios: AxiosInstance
|
||||
accessToken: string
|
||||
setAccessToken: (accessToken: string) => void
|
||||
refreshToken: string
|
||||
setRefreshToken: (refreshToken: string) => void
|
||||
}
|
||||
|
||||
const useAxios = create<AxiosHook>((set, get) => ({
|
||||
const useAxios = create<AxiosHook>(set => ({
|
||||
accessToken: '',
|
||||
setAccessToken: (accessToken: string) => {
|
||||
get().axios.defaults.headers.common['X-Access-Token'] = accessToken
|
||||
|
||||
taoAxios.defaults.headers.common['X-Access-Token'] = accessToken
|
||||
window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||
|
|
@ -24,8 +22,7 @@ const useAxios = create<AxiosHook>((set, get) => ({
|
|||
},
|
||||
refreshToken: '',
|
||||
setRefreshToken: (refreshToken: string) => {
|
||||
get().axios.defaults.headers.common['X-Refresh-Token'] = refreshToken
|
||||
|
||||
taoAxios.defaults.headers.common['X-Refresh-Token'] = refreshToken
|
||||
window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||
|
|
@ -33,33 +30,7 @@ const useAxios = create<AxiosHook>((set, get) => ({
|
|||
})
|
||||
|
||||
set({ refreshToken })
|
||||
},
|
||||
axios: axios.create({
|
||||
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
||||
timeout: 3000
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
useAxios.getState().axios.interceptors.response.use(
|
||||
res => res,
|
||||
async err => {
|
||||
const originalRequest = err.config
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
|
||||
return useAxios
|
||||
.getState()
|
||||
.axios.post('/auth/refresh', null, { withCredentials: true })
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
return useAxios.getState().axios(originalRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default useAxios
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { create } from 'zustand'
|
|||
import useAdb from './useAdb'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { fromUnixTime } from 'date-fns'
|
||||
import { Consumable, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||
import { Consumable, DecodeUtf8Stream, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||
import JSZip from 'jszip'
|
||||
import { type AndroidFile } from '@/models/android/schema'
|
||||
|
||||
|
|
@ -19,6 +19,8 @@ interface FileManagerAndroidHook {
|
|||
delete: (filename: string) => Promise<void>
|
||||
rename: (filename: string, newName: string) => Promise<void>
|
||||
download: (files: AndroidFile[]) => Promise<void>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readJsonFile: <TJson = any>(abPath: string) => Promise<TJson | undefined>
|
||||
}
|
||||
|
||||
const useFileManager = create<FileManagerAndroidHook>((set, get) => ({
|
||||
|
|
@ -258,6 +260,35 @@ const useFileManager = create<FileManagerAndroidHook>((set, get) => ({
|
|||
} finally {
|
||||
await sync.dispose()
|
||||
}
|
||||
},
|
||||
async readJsonFile<TJson>(abPath: string) {
|
||||
const adb = useAdb.getState().adb
|
||||
if (!adb) {
|
||||
toast({
|
||||
duration: 3000,
|
||||
variant: 'destructive',
|
||||
title: 'Failed to connect to device',
|
||||
description: 'Please connect Adb first'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const sync = await adb.sync()
|
||||
try {
|
||||
const fileStream = sync.read(abPath)
|
||||
let text: string = ''
|
||||
await fileStream.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
||||
new WritableStream<string>({
|
||||
write(chunk) {
|
||||
text += chunk
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return JSON.parse(text) as TJson
|
||||
} finally {
|
||||
await sync.dispose()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import { type RecipeDashboardFilterQuery } from './recipe-dashboard'
|
|||
interface localStorageHook {
|
||||
recipeQuery?: RecipeDashboardFilterQuery
|
||||
setRecipeQuery: (query?: RecipeDashboardFilterQuery) => void
|
||||
|
||||
layout: number[]
|
||||
setLayout: (layout: number[]) => void
|
||||
collapsed: boolean
|
||||
setCollapsed: (collapsed: boolean) => void
|
||||
// Add more local storage properties here
|
||||
}
|
||||
|
||||
const useLocalStorage = create(
|
||||
|
|
@ -13,6 +19,14 @@ const useLocalStorage = create(
|
|||
recipeQuery: undefined,
|
||||
setRecipeQuery(query) {
|
||||
set({ recipeQuery: query })
|
||||
},
|
||||
layout: [],
|
||||
setLayout(layout) {
|
||||
set({ layout })
|
||||
},
|
||||
collapsed: false,
|
||||
setCollapsed(collapsed) {
|
||||
set({ collapsed })
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { type RecipeDashboard } from '@/models/recipe/schema'
|
||||
import { create } from 'zustand'
|
||||
import useAxios from './axios'
|
||||
import taoAxios from '@/lib/taoAxios'
|
||||
|
||||
export interface RecipeDashboardFilterQuery {
|
||||
countryID: string
|
||||
|
|
@ -13,15 +13,21 @@ interface materialDashboard {
|
|||
}
|
||||
|
||||
interface RecipeDashboardHook {
|
||||
selectedRecipe: string
|
||||
setSelectedRecipe: (recipe: string) => void
|
||||
getRecipesDashboard: (filter?: RecipeDashboardFilterQuery) => Promise<RecipeDashboard[] | []>
|
||||
getMaterials: (filter?: RecipeDashboardFilterQuery) => Promise<materialDashboard[] | []>
|
||||
getRecipe: (productCode: string) => Promise<RecipeDashboard | null>
|
||||
}
|
||||
|
||||
const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
||||
selectedRecipe: '',
|
||||
setSelectedRecipe: id => {
|
||||
useRecipeDashboard.setState({ selectedRecipe: id })
|
||||
},
|
||||
async getRecipesDashboard(filter) {
|
||||
return useAxios
|
||||
.getState()
|
||||
.axios.get<RecipeDashboard[]>('/v2/recipes/dashboard', {
|
||||
return taoAxios
|
||||
.get<RecipeDashboard[]>('/v2/recipes/dashboard', {
|
||||
params: filter
|
||||
? {
|
||||
country_id: filter.countryID,
|
||||
|
|
@ -35,9 +41,8 @@ const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
|||
.catch(() => [])
|
||||
},
|
||||
async getMaterials(filter) {
|
||||
return useAxios
|
||||
.getState()
|
||||
.axios.get<materialDashboard[]>('/v2/materials/dashboard', {
|
||||
return taoAxios
|
||||
.get<materialDashboard[]>('/v2/materials/dashboard', {
|
||||
params: filter
|
||||
? {
|
||||
country_id: filter.countryID,
|
||||
|
|
@ -48,6 +53,14 @@ const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
|||
.then(res => {
|
||||
return res.data
|
||||
})
|
||||
},
|
||||
async getRecipe(productCode) {
|
||||
return taoAxios
|
||||
.get<RecipeDashboard>(`/v2/recipes/${productCode}`)
|
||||
.then(res => {
|
||||
return res.data
|
||||
})
|
||||
.catch(() => null)
|
||||
}
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { create } from 'zustand'
|
||||
import type { User } from '@/models/user/schema'
|
||||
import useAxios from './axios'
|
||||
import taoAxios from '@/lib/taoAxios'
|
||||
|
||||
interface UserAuth {
|
||||
userInfo: User | null
|
||||
|
|
@ -14,7 +14,7 @@ const userAuthStore = create<UserAuth>(set => ({
|
|||
setUserInfo: userInfo => set({ userInfo }),
|
||||
getUserInfo: async () => {
|
||||
try {
|
||||
const res = await useAxios.getState().axios.get<User>('/auth/me')
|
||||
const res = await taoAxios.get<User>('/auth/me')
|
||||
set({ userInfo: res.data })
|
||||
return res.data
|
||||
} catch {
|
||||
|
|
@ -23,7 +23,7 @@ const userAuthStore = create<UserAuth>(set => ({
|
|||
}
|
||||
},
|
||||
logout: () => {
|
||||
useAxios.getState().axios.post('/auth/logout')
|
||||
taoAxios.post('/auth/logout')
|
||||
set({ userInfo: null })
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -25,25 +25,36 @@ const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
|
|||
useEffect(() => {
|
||||
console.log('ENV:', import.meta.env.MODE)
|
||||
|
||||
// Sync keyChain
|
||||
const syncKeyChain = async () => {
|
||||
const tokens: {
|
||||
account: string
|
||||
password: string
|
||||
}[] = await window.ipcRenderer.invoke(
|
||||
'keyChainSync',
|
||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
|
||||
)
|
||||
if (tokens.length > 0) {
|
||||
const [accessToken, refreshToken] = tokens
|
||||
console.log(accessToken)
|
||||
console.log(refreshToken)
|
||||
useAxios.getState().setAccessToken(accessToken.password)
|
||||
useAxios.getState().setRefreshToken(refreshToken.password)
|
||||
if (window.electronRuntime) {
|
||||
// Sync keyChain
|
||||
const syncKeyChain = async () => {
|
||||
const tokens: {
|
||||
account: string
|
||||
password: string
|
||||
}[] = await window.ipcRenderer.invoke(
|
||||
'keyChainSync',
|
||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
|
||||
)
|
||||
if (tokens.length > 0) {
|
||||
const [accessToken, refreshToken] = tokens
|
||||
console.log(accessToken)
|
||||
console.log(refreshToken)
|
||||
useAxios.getState().setAccessToken(accessToken.password)
|
||||
useAxios.getState().setRefreshToken(refreshToken.password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncKeyChain().then(() => {
|
||||
syncKeyChain().then(() => {
|
||||
if (!userInfo) {
|
||||
getUserInfo().then(userInfo => {
|
||||
// if still not login then redirect to login page
|
||||
if (!userInfo && import.meta.env.PROD) {
|
||||
navigate('/login?redirect=' + currentPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (!userInfo) {
|
||||
getUserInfo().then(userInfo => {
|
||||
// if still not login then redirect to login page
|
||||
|
|
@ -52,7 +63,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [userInfo])
|
||||
|
||||
return (
|
||||
|
|
|
|||
54
client-electron/src/lib/taoAxios.ts
Normal file
54
client-electron/src/lib/taoAxios.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import axios, { type AxiosResponse } from 'axios'
|
||||
|
||||
const taoAxios = axios.create({
|
||||
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
taoAxios.interceptors.response.use(
|
||||
res => res,
|
||||
async err => {
|
||||
if (err.response.status === 401 && !err.config._retry) {
|
||||
const config = err.config
|
||||
config._retry = true
|
||||
|
||||
let res: AxiosResponse
|
||||
if (window.electronRuntime) {
|
||||
const refreshToken = await window.ipcRenderer.invoke('get-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
||||
})
|
||||
|
||||
console.log('refreshToken', refreshToken)
|
||||
|
||||
res = await taoAxios.get('/auth/refresh', {
|
||||
headers: {
|
||||
'X-Refresh-Token': refreshToken
|
||||
}
|
||||
})
|
||||
|
||||
await window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||
password: res.data.accessToken
|
||||
})
|
||||
|
||||
await window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||
password: res.data.refreshToken
|
||||
})
|
||||
} else {
|
||||
res = await taoAxios.get('/auth/refresh')
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
return taoAxios(config)
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
export default taoAxios
|
||||
|
|
@ -9,3 +9,203 @@ export const recipeDashboardSchema = z.object({
|
|||
image: z.string().nullable()
|
||||
})
|
||||
export type RecipeDashboard = z.infer<typeof recipeDashboardSchema>
|
||||
|
||||
export const matRecipeSchema = z.object({
|
||||
StringParam: z.string().nullable().or(z.undefined()),
|
||||
MixOrder: z.number(),
|
||||
FeedParameter: z.number(),
|
||||
FeedPattern: z.number(),
|
||||
isUse: z.boolean(),
|
||||
materialPathId: z.number(),
|
||||
powderGram: z.number(),
|
||||
powderTime: z.number(),
|
||||
stirTime: z.number(),
|
||||
syrupGram: z.number(),
|
||||
syrupTime: z.number(),
|
||||
waterCold: z.number(),
|
||||
waterYield: z.number()
|
||||
})
|
||||
|
||||
export type MatRecipe = z.infer<typeof matRecipeSchema>
|
||||
|
||||
export const toppingSetSchema = z.object({
|
||||
ListGroupID: z.array(z.number()).or(z.undefined()),
|
||||
defaultIDSelect: z.number(),
|
||||
groupID: z.string().or(z.undefined()),
|
||||
isUse: z.boolean()
|
||||
})
|
||||
|
||||
export type ToppingSet = z.infer<typeof toppingSetSchema>
|
||||
|
||||
const recipe01WithoutSubSchema = z.object({
|
||||
Description: z.string().or(z.undefined()),
|
||||
ExtendID: z.number(),
|
||||
OnTOP: z.boolean(),
|
||||
LastChange: z.string().pipe(z.coerce.date()).or(z.undefined()),
|
||||
MenuStatus: z.number(),
|
||||
StringParam: z.string().nullable().or(z.undefined()),
|
||||
TextForWarningBeforePay: z.array(z.string()).or(z.undefined()),
|
||||
cashPrice: z.number(),
|
||||
changerecipe: z.string().or(z.undefined()),
|
||||
disable: z.boolean(),
|
||||
disable_by_cup: z.boolean(),
|
||||
disable_by_ice: z.boolean(),
|
||||
EncoderCount: z.number(),
|
||||
id: z.number(),
|
||||
isUse: z.boolean(),
|
||||
isShow: z.boolean(),
|
||||
name: z.string().or(z.undefined()),
|
||||
nonCashPrice: z.number(),
|
||||
otherDescription: z.string().or(z.undefined()),
|
||||
otherName: z.string().or(z.undefined()),
|
||||
productCode: z.string(),
|
||||
recipes: z.array(matRecipeSchema),
|
||||
ToppingSet: z.array(toppingSetSchema),
|
||||
total_time: z.number(),
|
||||
total_weight: z.number(),
|
||||
uriData: z.string().or(z.undefined()),
|
||||
useGram: z.boolean(),
|
||||
weight_float: z.number()
|
||||
})
|
||||
|
||||
export const recipe01Schema = z.object({
|
||||
Description: z.string().or(z.undefined()),
|
||||
ExtendID: z.number(),
|
||||
OnTOP: z.boolean(),
|
||||
LastChange: z.string().pipe(z.coerce.date()).or(z.undefined()),
|
||||
MenuStatus: z.number(),
|
||||
StringParam: z.string().nullable().or(z.undefined()),
|
||||
TextForWarningBeforePay: z.array(z.string()).or(z.undefined()),
|
||||
cashPrice: z.number(),
|
||||
changerecipe: z.string().or(z.undefined()),
|
||||
disable: z.boolean(),
|
||||
disable_by_cup: z.boolean(),
|
||||
disable_by_ice: z.boolean(),
|
||||
EncoderCount: z.number(),
|
||||
id: z.number(),
|
||||
isUse: z.boolean(),
|
||||
isShow: z.boolean(),
|
||||
name: z.string().or(z.undefined()),
|
||||
nonCashPrice: z.number(),
|
||||
otherDescription: z.string().or(z.undefined()),
|
||||
otherName: z.string().or(z.undefined()),
|
||||
productCode: z.string(),
|
||||
recipes: z.array(matRecipeSchema),
|
||||
SubMenu: z.array(recipe01WithoutSubSchema),
|
||||
ToppingSet: z.array(toppingSetSchema),
|
||||
total_time: z.number(),
|
||||
total_weight: z.number(),
|
||||
uriData: z.string().or(z.undefined()),
|
||||
useGram: z.boolean(),
|
||||
weight_float: z.number()
|
||||
})
|
||||
|
||||
export type Recipe01 = z.infer<typeof recipe01Schema>
|
||||
|
||||
export const materialSettingSchema = z.object({
|
||||
materialName: z.string(),
|
||||
materialId: z.number().or(z.undefined()),
|
||||
materialOtherName: z.string(),
|
||||
RawMaterialUnit: z.string().or(z.undefined()),
|
||||
IceScreamBingsuChannel: z.boolean(),
|
||||
StringParam: z.string().nullable().or(z.undefined()),
|
||||
AlarmIDWhenOffline: z.number(),
|
||||
BeanChannel: z.boolean(),
|
||||
CanisterType: z.string().or(z.undefined()),
|
||||
DrainTimer: z.number(),
|
||||
IsEquipment: z.boolean(),
|
||||
LeavesChannel: z.boolean(),
|
||||
LowToOffline: z.number(),
|
||||
MaterialStatus: z.number(),
|
||||
PowderChannel: z.boolean(),
|
||||
RefillUnitGram: z.boolean(),
|
||||
RefillUnitMilliliters: z.boolean(),
|
||||
RefillUnitPCS: z.boolean(),
|
||||
ScheduleDrainType: z.number(),
|
||||
SodaChannel: z.boolean(),
|
||||
StockAdjust: z.string().or(z.undefined()),
|
||||
SyrupChannel: z.boolean(),
|
||||
id: z.number(),
|
||||
idAlternate: z.number(),
|
||||
isUse: z.boolean(),
|
||||
pay_rettry_max_count: z.number(),
|
||||
feed_mode: z.string().or(z.undefined()),
|
||||
MaterialParameter: z.string().or(z.undefined())
|
||||
})
|
||||
|
||||
export type MaterialSetting = z.infer<typeof materialSettingSchema>
|
||||
|
||||
export const machineSettingSchema = z.object({
|
||||
RecipeTag: z.string(),
|
||||
StrTextShowError: z.array(z.string().nullable()),
|
||||
configNumber: z.number(),
|
||||
temperatureMax: z.number(),
|
||||
temperatureMin: z.number()
|
||||
})
|
||||
|
||||
export type MachineSetting = z.infer<typeof machineSettingSchema>
|
||||
|
||||
export const toppingGroupSchema = z.object({
|
||||
Desc: z.string().or(z.undefined()),
|
||||
groupID: z.number(),
|
||||
idDefault: z.number(),
|
||||
idInGroup: z.string(),
|
||||
inUse: z.boolean(),
|
||||
name: z.string(),
|
||||
otherName: z.string()
|
||||
})
|
||||
|
||||
export type ToppingGroup = z.infer<typeof toppingGroupSchema>
|
||||
|
||||
export const toppingListSchema = z.object({
|
||||
Extends: z.string().or(z.undefined()),
|
||||
OnTOP: z.boolean(),
|
||||
MenuStatus: z.number(),
|
||||
cashPrice: z.number(),
|
||||
disable: z.boolean(),
|
||||
disable_by_cup: z.boolean(),
|
||||
disable_by_ice: z.boolean(),
|
||||
EncoderCount: z.number(),
|
||||
id: z.number(),
|
||||
isUse: z.boolean(),
|
||||
isShow: z.boolean(),
|
||||
StringParam: z.string().nullable().or(z.undefined()),
|
||||
name: z.string().or(z.undefined()),
|
||||
nonCashPrice: z.number(),
|
||||
otherName: z.string().or(z.undefined()),
|
||||
productCode: z.string().or(z.undefined()),
|
||||
recipes: z.array(matRecipeSchema),
|
||||
total_time: z.number(),
|
||||
total_weight: z.number(),
|
||||
useGram: z.boolean(),
|
||||
weight_float: z.number()
|
||||
})
|
||||
|
||||
export type ToppingList = z.infer<typeof toppingListSchema>
|
||||
|
||||
export const toppingSchema = z.object({
|
||||
ToppingGroup: z.array(toppingGroupSchema),
|
||||
ToppingList: z.array(toppingListSchema)
|
||||
})
|
||||
|
||||
export type Topping = z.infer<typeof toppingSchema>
|
||||
|
||||
export const materialCodeSchema = z.object({
|
||||
PackageDescription: z.string().or(z.undefined()),
|
||||
RefillValuePerStep: z.number(),
|
||||
materialID: z.number(),
|
||||
materialCode: z.string().or(z.undefined())
|
||||
})
|
||||
|
||||
export type MaterialCode = z.infer<typeof materialCodeSchema>
|
||||
|
||||
export const recipesSchema = z.object({
|
||||
Timestamp: z.string(),
|
||||
Recipe01: z.array(recipe01Schema),
|
||||
MachineSetting: machineSettingSchema,
|
||||
Topping: toppingSchema,
|
||||
MaterialCode: z.array(materialCodeSchema),
|
||||
MaterialSetting: z.array(materialSettingSchema)
|
||||
})
|
||||
|
||||
export type Recipes = z.infer<typeof recipesSchema>
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { RocketIcon } from '@radix-ui/react-icons'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>This is Home Page!!!!!!!</h1>
|
||||
<Button asChild>
|
||||
<Link to="/android">
|
||||
<RocketIcon className="mr-2 h-5 w-5" />
|
||||
Go to Android Page
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
|
|
@ -7,7 +7,7 @@ const LoginPage: React.FC = () => {
|
|||
const setUserInfo = userAuthStore(state => state.setUserInfo)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const redirectUrl = new URLSearchParams(window.location.search).get('redirect_to') ?? '/'
|
||||
const redirectUrl = new URLSearchParams(window.location.search).get('redirect_to') ?? '/recipes'
|
||||
|
||||
const loginWithGoogle = () => {
|
||||
// if is web mode then use window.open
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import useFileManager from '@/hooks/filemanager-android'
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import DataTable from './filemanager-table/data-table'
|
||||
import { FileIcon } from '@radix-ui/react-icons'
|
||||
import { type ColumnDef } from '@tanstack/react-table'
|
||||
import { LinuxFileType } from '@yume-chan/adb'
|
||||
import { formatDate } from 'date-fns'
|
||||
|
|
@ -11,6 +10,7 @@ import DataTableColumnHeader from './filemanager-table/data-table-column-header'
|
|||
import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { type AndroidFile } from '@/models/android/schema'
|
||||
import { File, Folder } from 'lucide-react'
|
||||
|
||||
export const FileManagerTab: React.FC = () => {
|
||||
const { currentPath, pushPath, scanPath } = useFileManager(
|
||||
|
|
@ -61,6 +61,13 @@ export const FileManagerTab: React.FC = () => {
|
|||
accessorKey: 'filename',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => {
|
||||
let icon: React.ReactNode
|
||||
if (row.original.type === LinuxFileType.File) {
|
||||
icon = <File size={20} strokeWidth={1} className="mr-2" />
|
||||
} else {
|
||||
icon = <Folder size={20} strokeWidth={1} className="mr-2" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex hover:cursor-pointer"
|
||||
|
|
@ -68,8 +75,8 @@ export const FileManagerTab: React.FC = () => {
|
|||
if (row.original.type !== LinuxFileType.File) pushPath(row.original.filename)
|
||||
}}
|
||||
>
|
||||
{row.original.type === LinuxFileType.File && <FileIcon className="mr-2 h-4 w-4" />}
|
||||
<span className={row.original.type !== LinuxFileType.File ? 'hover:underline' : 'ml-2'}>
|
||||
{icon}
|
||||
<span className={row.original.type !== LinuxFileType.File ? 'hover:underline' : ''}>
|
||||
{row.original.filename}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import useAndroidSwitcher from '@/hooks/android-switcher'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface AndroidSwitcherProps {
|
||||
isCollapsed?: boolean
|
||||
androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
}
|
||||
|
||||
const AndroidSwitcher: React.FC<AndroidSwitcherProps> = ({ androids, isCollapsed }) => {
|
||||
const { selectedAndroid, setSelectedAndroid } = useAndroidSwitcher(
|
||||
useShallow(state => ({
|
||||
selectedAndroid: state.selectedAndroid,
|
||||
setSelectedAndroid: state.setSelectedAndroid
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<Select defaultValue={selectedAndroid} onValueChange={setSelectedAndroid}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
||||
isCollapsed && 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
||||
)}
|
||||
aria-label="Select android"
|
||||
>
|
||||
<SelectValue placeholder="Select an android">
|
||||
{androids.find(android => android.serial === selectedAndroid)?.icon}
|
||||
<span className={cn('ml-2', isCollapsed && 'hidden')}>
|
||||
{androids.find(android => android.serial === selectedAndroid)?.label}
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{androids.map(android => (
|
||||
<SelectItem key={android.serial} value={android.serial}>
|
||||
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
|
||||
{android.icon}
|
||||
{android.deviceName}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
export default AndroidSwitcher
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type LucideIcon } from 'lucide-react'
|
||||
|
||||
interface NavProps {
|
||||
isCollapsed: boolean
|
||||
links: {
|
||||
index: number
|
||||
title: string
|
||||
label?: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
showListIndex: number
|
||||
setShowListIndex: (index: number) => void
|
||||
}
|
||||
|
||||
const Nav: React.FC<NavProps> = ({ links, isCollapsed, showListIndex, setShowListIndex }) => {
|
||||
return (
|
||||
<div data-collapsed={isCollapsed} className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2">
|
||||
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||
{links.map((link, index) =>
|
||||
isCollapsed ? (
|
||||
<Tooltip key={index} delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
key={index}
|
||||
variant={showListIndex === link.index ? 'default' : 'ghost'}
|
||||
size={'icon'}
|
||||
className={cn(
|
||||
'h-9 w-9',
|
||||
showListIndex === link.index &&
|
||||
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
|
||||
)}
|
||||
onClick={() => setShowListIndex(link.index)}
|
||||
>
|
||||
<link.icon className="h-4 w-4" />
|
||||
<span className="sr-only">{link.title}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex items-center gap-4">
|
||||
{link.title}
|
||||
{link.label && <span className="ml-auto text-muted-foreground">{link.label}</span>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
key={index}
|
||||
variant={showListIndex === link.index ? 'default' : 'ghost'}
|
||||
size={'sm'}
|
||||
className={cn(
|
||||
showListIndex === link.index &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start'
|
||||
)}
|
||||
onClick={() => setShowListIndex(link.index)}
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{link.title}
|
||||
{link.label && (
|
||||
<span className={cn('ml-auto', showListIndex === link.index && 'text-background dark:text-white')}>
|
||||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import { type Recipes } from '@/models/recipe/schema'
|
||||
import { format } from 'date-fns'
|
||||
import { Archive, ArchiveX, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface RecipeDisplayProps {
|
||||
recipes: Recipes
|
||||
}
|
||||
|
||||
const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||
const selectedRecipe = useRecipeDashboard(state => state.selectedRecipe)
|
||||
|
||||
const recipe = useMemo(() => {
|
||||
return recipes.Recipe01.find(recipe => recipe.productCode === selectedRecipe)
|
||||
}, [selectedRecipe])
|
||||
|
||||
const user:
|
||||
| {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
| undefined = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john_doe@gmail.com'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Archive className="h-4 w-4" />
|
||||
<span className="sr-only">Archive</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Archive</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<ArchiveX className="h-4 w-4" />
|
||||
<span className="sr-only">Move to junk</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to junk</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">Move to trash</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to trash</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Reply className="h-4 w-4" />
|
||||
<span className="sr-only">Reply</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<ReplyAll className="h-4 w-4" />
|
||||
<span className="sr-only">Reply all</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply all</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Forward className="h-4 w-4" />
|
||||
<span className="sr-only">Forward</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Forward</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-2 h-6" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
|
||||
<DropdownMenuItem>Star thread</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add label</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mute thread</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Separator />
|
||||
{user ? (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex items-start p-4">
|
||||
<div className="flex items-start gap-4 text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage alt={user.name} />
|
||||
<AvatarFallback>
|
||||
{user.name
|
||||
.split(' ')
|
||||
.map(chunk => chunk[0])
|
||||
.join('')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<div className="font-semibold">{user.name}</div>
|
||||
<div className="line-clamp-1 text-xs">
|
||||
<span className="font-medium">ID:</span> {user.id}
|
||||
</div>
|
||||
<div className="line-clamp-1 text-xs">
|
||||
<span className="font-medium">Email:</span> {user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{recipe && recipe.LastChange && (
|
||||
<div className="ml-auto text-xs text-muted-foreground">{format(new Date(recipe.LastChange), 'PPpp')}</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
|
||||
{/* show name and productCode of recipe */}
|
||||
{recipe && (
|
||||
<div>
|
||||
<div className="font-semibold">{recipe.name}</div>
|
||||
<div className="text-xs">{recipe.productCode}</div>
|
||||
</div>
|
||||
)}
|
||||
{/* show recipe description */}
|
||||
{recipe && <div className="mt-4">{recipe.Description}</div>}
|
||||
</div>
|
||||
<Separator className="mt-auto" />
|
||||
<div className="p-4">
|
||||
<form>
|
||||
<div className="grid gap-4">
|
||||
<Textarea className="p-4" placeholder={`Reply ${`John Doe`}...`} />
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="mute" className="flex items-center gap-2 text-xs font-normal">
|
||||
<Switch id="mute" aria-label="Mute thread" /> Mute this thread
|
||||
</Label>
|
||||
<Button onClick={e => e.preventDefault()} size="sm" className="ml-auto">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">No message selected</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeDisplay
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type Recipe01, type Recipes } from '@/models/recipe/schema'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import AndroidSwitcher from './android-switcher'
|
||||
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff } from 'lucide-react'
|
||||
import Nav from './nav'
|
||||
import RecipeList from './recipe-list'
|
||||
import { isBefore, isToday } from 'date-fns'
|
||||
import RecipeDisplay from './recipe-display'
|
||||
|
||||
interface RecipeMenuProps {
|
||||
recipes: Recipes
|
||||
recipe01: Recipe01[]
|
||||
defaultSize?: number
|
||||
isDevBranch: boolean
|
||||
}
|
||||
const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, defaultSize, isDevBranch }) => {
|
||||
recipe01 = useMemo(() => {
|
||||
return recipe01.sort((a, b) => (a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1))
|
||||
}, [recipe01])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanel defaultSize={defaultSize} minSize={30}>
|
||||
<Tabs defaultValue="all">
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<h1 className="text-xl font-bold">
|
||||
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
|
||||
</h1>
|
||||
<TabsList className="ml-auto">
|
||||
<TabsTrigger value="all" className="text-zinc-600 dark:text-zinc-200">
|
||||
All Menu
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="today" className="text-zinc-600 dark:text-zinc-200">
|
||||
Today
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search" className="pl-8" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<TabsContent value="all" className="m-0">
|
||||
<RecipeList items={recipe01} />
|
||||
</TabsContent>
|
||||
<TabsContent value="today" className="m-0">
|
||||
<RecipeList items={recipe01.filter(item => item.LastChange && isToday(item.LastChange))} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={defaultSize}>
|
||||
<RecipeDisplay recipes={recipes} />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
interface RecipeEditorProps {
|
||||
androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
isDevBranch: boolean
|
||||
recipes: Recipes
|
||||
defaultLayout: number[] | undefined
|
||||
defaultCollapsed?: boolean
|
||||
navCollapsedSize: number
|
||||
}
|
||||
|
||||
export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||
androids,
|
||||
recipes,
|
||||
isDevBranch,
|
||||
defaultLayout = [265, 440, 655],
|
||||
defaultCollapsed = false,
|
||||
navCollapsedSize
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
|
||||
|
||||
const [showListIndex, setShowListIndex] = useState(0)
|
||||
|
||||
const { recipesEnable, recipeDisable } = useMemo(() => {
|
||||
return {
|
||||
recipesEnable: recipes.Recipe01.filter(r => r.isUse),
|
||||
recipeDisable: recipes.Recipe01.filter(r => !r.isUse)
|
||||
}
|
||||
}, [recipes])
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes: number[]) => {
|
||||
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
||||
}}
|
||||
className="h-full max-h-[800px] items-stretch"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={defaultLayout[0]}
|
||||
collapsedSize={navCollapsedSize}
|
||||
collapsible={true}
|
||||
minSize={15}
|
||||
maxSize={20}
|
||||
onCollapse={() => {
|
||||
setIsCollapsed(true)
|
||||
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`
|
||||
}}
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false)
|
||||
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`
|
||||
}}
|
||||
className={cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out')}
|
||||
>
|
||||
<div className={cn('flex h-[52px] items-center justify-center', isCollapsed ? 'h-[52px]' : 'px-2')}>
|
||||
<AndroidSwitcher isCollapsed={isCollapsed} androids={androids} />
|
||||
</div>
|
||||
<Separator />
|
||||
<Nav
|
||||
isCollapsed={isCollapsed}
|
||||
showListIndex={showListIndex}
|
||||
setShowListIndex={setShowListIndex}
|
||||
links={[
|
||||
{
|
||||
index: 0,
|
||||
title: 'Menu (Enabled)',
|
||||
label: recipesEnable.length.toString(),
|
||||
icon: CupSoda
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
title: 'Menu (Disabled)',
|
||||
label: recipeDisable.length.toString(),
|
||||
icon: WineOff
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Separator />
|
||||
<Nav
|
||||
isCollapsed={isCollapsed}
|
||||
showListIndex={showListIndex}
|
||||
setShowListIndex={setShowListIndex}
|
||||
links={[
|
||||
{
|
||||
index: 2,
|
||||
title: 'Materials',
|
||||
label: recipes.MaterialSetting.length.toString(),
|
||||
icon: Wheat
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
title: 'ToppingsGroups',
|
||||
label: recipes.Topping.ToppingGroup.length.toString(),
|
||||
icon: Dessert
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
title: 'ToppingsList',
|
||||
label: recipes.Topping.ToppingList.length.toString(),
|
||||
icon: Cherry
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
{showListIndex === 0 ? (
|
||||
<RecipeMenu
|
||||
recipes={recipes}
|
||||
recipe01={recipesEnable}
|
||||
defaultSize={defaultLayout[1]}
|
||||
isDevBranch={isDevBranch}
|
||||
/>
|
||||
) : showListIndex === 1 ? (
|
||||
<RecipeMenu
|
||||
recipes={recipes}
|
||||
recipe01={recipeDisable}
|
||||
defaultSize={defaultLayout[1]}
|
||||
isDevBranch={isDevBranch}
|
||||
/>
|
||||
) : null}
|
||||
</ResizablePanelGroup>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeEditor
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type Recipe01 } from '@/models/recipe/schema'
|
||||
import { formatDistanceToNow, isToday } from 'date-fns'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface RecipeListProps {
|
||||
items: Recipe01[]
|
||||
}
|
||||
|
||||
const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
|
||||
const { selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
|
||||
useShallow(state => ({
|
||||
selectedRecipe: state.selectedRecipe,
|
||||
setSelectedRecipe: state.setSelectedRecipe
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-screen">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.productCode}
|
||||
className={cn(
|
||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
|
||||
selectedRecipe === item.productCode && 'bg-muted'
|
||||
)}
|
||||
onClick={() => setSelectedRecipe(item.productCode)}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold">{item.name}</div>
|
||||
{item.LastChange && isToday(item.LastChange) && (
|
||||
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'ml-auto text-xs',
|
||||
selectedRecipe === item.productCode ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{item.LastChange &&
|
||||
formatDistanceToNow(new Date(item.LastChange), {
|
||||
addSuffix: true
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium">{`${item.productCode}: ${item.name}`}</div>
|
||||
</div>
|
||||
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
|
||||
{item.labels.length ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.labels.map(label => (
|
||||
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
|
||||
{label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null} */}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeList
|
||||
|
|
@ -1,27 +1,104 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { columns } from './components/recipe-table-components/columns'
|
||||
import DataTable from './components/recipe-table-components/data-table'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
import RecipeEditor from './components/recipe-editor-components/recipe-editor'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { Smartphone } from 'lucide-react'
|
||||
import { type Recipes } from '@/models/recipe/schema'
|
||||
import useFileManager from '@/hooks/filemanager-android'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useAdb from '@/hooks/useAdb'
|
||||
import { isBefore } from 'date-fns'
|
||||
|
||||
const androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[] = [
|
||||
{
|
||||
label: 'Test Device',
|
||||
deviceName: 'Test Device',
|
||||
serial: 'Test Device',
|
||||
icon: <Smartphone />
|
||||
}
|
||||
]
|
||||
|
||||
const RecipesTablePage = () => {
|
||||
const recipeQuery = useLocalStorage(state => state.recipeQuery)
|
||||
const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
|
||||
// const recipeQuery = useLocalStorage(state => state.recipeQuery)
|
||||
// const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
|
||||
|
||||
const { data: recipeDashboardList, isLoading } = useQuery({
|
||||
queryKey: ['recipe-overview'],
|
||||
queryFn: () => getRecipesDashboard(recipeQuery)
|
||||
})
|
||||
// const { data: recipeDashboardList, isLoading } = useQuery({
|
||||
// queryKey: ['recipe-overview'],
|
||||
// queryFn: () => getRecipesDashboard(recipeQuery)
|
||||
// })
|
||||
|
||||
const { layout, collapsed } = useLocalStorage(
|
||||
useShallow(state => ({
|
||||
layout: state.layout,
|
||||
collapsed: state.collapsed
|
||||
}))
|
||||
)
|
||||
|
||||
const adb = useAdb(state => state.adb)
|
||||
|
||||
const readJsonFile = useFileManager(state => state.readJsonFile)
|
||||
|
||||
const [recipes, setRecipes] = useState<Recipes>()
|
||||
|
||||
const [isDevBranch, setIsDevBranch] = useState(false)
|
||||
|
||||
const readData = useCallback(async () => {
|
||||
try {
|
||||
return await readJsonFile<Recipes>('/sdcard/coffeevending/cfg/recipe_branch_dev.json').then(data => {
|
||||
if (data) {
|
||||
setIsDevBranch(true)
|
||||
data.Recipe01 = data.Recipe01.sort((a, b) =>
|
||||
a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1
|
||||
)
|
||||
}
|
||||
return data
|
||||
})
|
||||
} catch (e) {
|
||||
return await readJsonFile<Recipes>('/sdcard/coffeevending/coffeethai02.json').then(data => {
|
||||
if (data) {
|
||||
setIsDevBranch(false)
|
||||
data.Recipe01 = data.Recipe01.sort((a, b) =>
|
||||
a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1
|
||||
)
|
||||
}
|
||||
return data
|
||||
})
|
||||
}
|
||||
}, [adb])
|
||||
|
||||
useEffect(() => {
|
||||
readData().then(data => {
|
||||
setRecipes(data)
|
||||
})
|
||||
}, [readData])
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<section>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
</section>
|
||||
<section>
|
||||
<DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
|
||||
</section>
|
||||
</div>
|
||||
// <div className="flex w-full flex-col gap-3">
|
||||
// <section>
|
||||
// <h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
// </section>
|
||||
// <section>
|
||||
// <DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
|
||||
// </section>
|
||||
// </div>
|
||||
<>
|
||||
{recipes ? (
|
||||
<RecipeEditor
|
||||
androids={androids}
|
||||
defaultLayout={layout}
|
||||
navCollapsedSize={4}
|
||||
recipes={recipes}
|
||||
isDevBranch={isDevBranch}
|
||||
defaultCollapsed={collapsed}
|
||||
/>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
const SelectCountryPage: React.FC = () => {
|
||||
const { recipeQuery, setRecipeQuery } = useLocalStorage(
|
||||
useShallow(state => ({
|
||||
recipeQuery: state.recipeQuery,
|
||||
setRecipeQuery: state.setRecipeQuery
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>SelectContryPage</h1>
|
||||
<Button variant={'link'} onClick={() => setRecipeQuery({ countryID: 'tha', filename: 'coffeethai02_635.json' })}>
|
||||
Thai
|
||||
</Button>
|
||||
{recipeQuery && <Link to={`/recipes/${recipeQuery?.countryID}/${recipeQuery?.filename}`}>Recipes</Link>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectCountryPage
|
||||
Loading…
Add table
Add a link
Reference in a new issue