add recipe viewer

This commit is contained in:
Kenta420 2024-02-21 15:17:54 +07:00
parent 92b11f7b9d
commit f7f1535695
31 changed files with 1532 additions and 151 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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 />
},
{

View 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

View file

@ -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 (

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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

View file

@ -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

View file

@ -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()
}
}
}))

View file

@ -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 })
}
}),
{

View file

@ -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)
}
}))

View file

@ -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 })
}
}))

View file

@ -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 (

View 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

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>
)}
</>
)
}

View file

@ -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