Update: Something spicy for recipe viewing only but called it editor lol
This commit is contained in:
parent
72187f348b
commit
7dd075dd59
8 changed files with 450 additions and 346 deletions
165
client-electron/electron/adb-native.ts
Normal file
165
client-electron/electron/adb-native.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||||
|
import { type AdbDaemonWebUsbDevice, AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
import { type BrowserWindow } from 'electron'
|
||||||
|
import { WebUSB } from 'usb'
|
||||||
|
|
||||||
|
import { type AdbCredentialStore, adbGeneratePublicKey } from '@yume-chan/adb'
|
||||||
|
import { webcrypto } from 'node:crypto'
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { homedir, hostname, userInfo } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
class AdbNodeJsCredentialStore implements AdbCredentialStore {
|
||||||
|
#name: string
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.#name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
#privateKeyPath() {
|
||||||
|
return join(homedir(), '.android', 'adbkey')
|
||||||
|
}
|
||||||
|
|
||||||
|
#publicKeyPath() {
|
||||||
|
return join(homedir(), '.android', 'adbkey.pub')
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateKey() {
|
||||||
|
const { privateKey: cryptoKey } = await webcrypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
|
modulusLength: 2048,
|
||||||
|
// 65537
|
||||||
|
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
||||||
|
hash: 'SHA-1'
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['sign', 'verify']
|
||||||
|
)
|
||||||
|
|
||||||
|
const privateKey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', cryptoKey))
|
||||||
|
await writeFile(this.#privateKeyPath(), Buffer.from(privateKey).toString('utf8'))
|
||||||
|
await writeFile(
|
||||||
|
this.#publicKeyPath(),
|
||||||
|
`${Buffer.from(adbGeneratePublicKey(privateKey)).toString('base64')} ${this.#name}\n`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: privateKey,
|
||||||
|
name: this.#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #readPubKeyName() {
|
||||||
|
const content = await readFile(this.#publicKeyPath(), 'utf8')
|
||||||
|
const pubKeyName = content.split(' ')[1]
|
||||||
|
return pubKeyName || `${userInfo().username}@${hostname()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async *iterateKeys() {
|
||||||
|
const content = await readFile(this.#privateKeyPath(), 'utf8')
|
||||||
|
const privateKey = Buffer.from(content.split('\n').slice(1, -2).join(''), 'base64')
|
||||||
|
yield {
|
||||||
|
buffer: privateKey,
|
||||||
|
name: await this.#readPubKeyName()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectedDevice = {
|
||||||
|
[serial: string]: {
|
||||||
|
adb: Adb
|
||||||
|
metadata: {
|
||||||
|
venderId?: number
|
||||||
|
productId?: number
|
||||||
|
manufacturerNam?: string
|
||||||
|
productName?: string
|
||||||
|
serialNumber?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdbUsbNative {
|
||||||
|
private _manager: AdbDaemonWebUsbDeviceManager
|
||||||
|
private _win: BrowserWindow
|
||||||
|
|
||||||
|
private _connectedDevices: ConnectedDevice = {}
|
||||||
|
|
||||||
|
constructor(win: BrowserWindow) {
|
||||||
|
const webUsb: WebUSB = new WebUSB({ allowedDevices: [{ serialNumber: 'd' }] })
|
||||||
|
this._manager = new AdbDaemonWebUsbDeviceManager(webUsb)
|
||||||
|
this._win = win
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableDevices(responseChannel: string) {
|
||||||
|
this._manager.getDevices().then(devices => {
|
||||||
|
this._win.webContents.send(
|
||||||
|
responseChannel,
|
||||||
|
devices.map(d => ({
|
||||||
|
vendorId: d.raw.vendorId,
|
||||||
|
productId: d.raw.productId,
|
||||||
|
manufacturerNam: d.raw.manufacturerName,
|
||||||
|
productName: d.raw.productName,
|
||||||
|
serialNumber: d.raw.serialNumber
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectedDevices(responseChannel: string) {
|
||||||
|
this._win.webContents.send(
|
||||||
|
responseChannel,
|
||||||
|
Object.entries(this._connectedDevices).map(([serial, { metadata }]) => ({
|
||||||
|
serial: serial,
|
||||||
|
productId: metadata.productId,
|
||||||
|
venderId: metadata.venderId,
|
||||||
|
manufacturerNam: metadata.manufacturerNam,
|
||||||
|
productName: metadata.productName
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
connectDevice(responseChanel: string, serial: string) {
|
||||||
|
this._manager.getDevices().then(devices => {
|
||||||
|
let device: AdbDaemonWebUsbDevice | null = null
|
||||||
|
for (const d of devices) {
|
||||||
|
if (d.serial === serial) {
|
||||||
|
device = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
this._win.webContents.send(responseChanel, {
|
||||||
|
error: 'Device with serialNumber: ' + serial + ' not found'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device.connect().then(connection => {
|
||||||
|
const credentialStore = new AdbNodeJsCredentialStore(`${userInfo().username}@${hostname()}`)
|
||||||
|
|
||||||
|
AdbDaemonTransport.authenticate({
|
||||||
|
serial: device!.serial,
|
||||||
|
connection,
|
||||||
|
credentialStore
|
||||||
|
}).then(transport => {
|
||||||
|
this._connectedDevices[device!.serial] = {
|
||||||
|
adb: new Adb(transport),
|
||||||
|
metadata: {
|
||||||
|
venderId: device!.raw.vendorId,
|
||||||
|
productId: device!.raw.productId,
|
||||||
|
manufacturerNam: device!.raw.manufacturerName,
|
||||||
|
productName: device!.raw.productName,
|
||||||
|
serialNumber: device!.raw.serialNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._win.webContents.send(responseChanel, {
|
||||||
|
serial: device!.serial
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
client-electron/package-lock.json
generated
14
client-electron/package-lock.json
generated
|
|
@ -65,7 +65,7 @@
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usb": "^2.11.0",
|
"usb": "^2.12.1",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
|
|
@ -13342,9 +13342,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/usb": {
|
"node_modules/usb": {
|
||||||
"version": "2.11.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/usb/-/usb-2.12.1.tgz",
|
||||||
"integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==",
|
"integrity": "sha512-hgtoSQUFuMXVJBApelpUTiX7ZB83MQCbYeHTBsHftA2JG7YZ76ycwIgKQhkhKqVY76C8K6xJscHpF7Ep0eG3pQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/w3c-web-usb": "^1.0.6",
|
"@types/w3c-web-usb": "^1.0.6",
|
||||||
|
|
@ -23341,9 +23341,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"usb": {
|
"usb": {
|
||||||
"version": "2.11.0",
|
"version": "2.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/usb/-/usb-2.12.1.tgz",
|
||||||
"integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==",
|
"integrity": "sha512-hgtoSQUFuMXVJBApelpUTiX7ZB83MQCbYeHTBsHftA2JG7YZ76ycwIgKQhkhKqVY76C8K6xJscHpF7Ep0eG3pQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/w3c-web-usb": "^1.0.6",
|
"@types/w3c-web-usb": "^1.0.6",
|
||||||
"node-addon-api": "^7.0.0",
|
"node-addon-api": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usb": "^2.11.0",
|
"usb": "^2.12.1",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,26 @@ interface materialDashboard {
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ItemMetadata = {
|
||||||
|
id: string | number
|
||||||
|
name?: string
|
||||||
|
lastChange?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListMetadata = {
|
||||||
|
items: ItemMetadata[]
|
||||||
|
currentSelectedId: string | number | undefined
|
||||||
|
onSelectFn: (id: string | number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EditorShowStateEnum {
|
||||||
|
RECIPES_IN_USE = 0,
|
||||||
|
RECIPES_NOT_IN_USE = 1,
|
||||||
|
MATERIALS_SETTING = 2,
|
||||||
|
TOPPING_GROUPS = 3,
|
||||||
|
TOPPING_LIST = 4
|
||||||
|
}
|
||||||
|
|
||||||
interface RecipeDashboardHook {
|
interface RecipeDashboardHook {
|
||||||
selectedRecipe: string
|
selectedRecipe: string
|
||||||
setSelectedRecipe: (recipe: string) => void
|
setSelectedRecipe: (recipe: string) => void
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
|
import { type EditorShowStateEnum } from '@/hooks/recipe-dashboard'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { type LucideIcon } from 'lucide-react'
|
import { type LucideIcon } from 'lucide-react'
|
||||||
|
|
||||||
interface NavProps {
|
interface NavProps {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
links: {
|
links: {
|
||||||
index: number
|
index: EditorShowStateEnum
|
||||||
title: string
|
title: string
|
||||||
label?: string
|
label?: string
|
||||||
icon: LucideIcon
|
icon: LucideIcon
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||||
import { type Recipes } from '@/models/recipe/schema'
|
import { type Recipes } from '@/models/recipe/schema'
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { Archive, ArchiveX, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-react'
|
import { Archive, ArchiveX, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
|
@ -25,25 +20,13 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
return recipes.Recipe01.find(recipe => recipe.productCode === selectedRecipe)
|
return recipes.Recipe01.find(recipe => recipe.productCode === selectedRecipe)
|
||||||
}, [selectedRecipe])
|
}, [selectedRecipe])
|
||||||
|
|
||||||
const user:
|
|
||||||
| {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
}
|
|
||||||
| undefined = {
|
|
||||||
id: '1',
|
|
||||||
name: 'John Doe',
|
|
||||||
email: 'john_doe@gmail.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col overflow-y-auto">
|
<div className="flex h-full flex-col overflow-y-auto">
|
||||||
<div className="flex items-center p-2">
|
<div className="flex items-center p-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<Archive className="h-4 w-4" />
|
<Archive className="h-4 w-4" />
|
||||||
<span className="sr-only">Archive</span>
|
<span className="sr-only">Archive</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -52,7 +35,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<ArchiveX className="h-4 w-4" />
|
<ArchiveX className="h-4 w-4" />
|
||||||
<span className="sr-only">Move to junk</span>
|
<span className="sr-only">Move to junk</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -61,7 +44,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
<span className="sr-only">Move to trash</span>
|
<span className="sr-only">Move to trash</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -73,7 +56,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<Reply className="h-4 w-4" />
|
<Reply className="h-4 w-4" />
|
||||||
<span className="sr-only">Reply</span>
|
<span className="sr-only">Reply</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -82,7 +65,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<ReplyAll className="h-4 w-4" />
|
<ReplyAll className="h-4 w-4" />
|
||||||
<span className="sr-only">Reply all</span>
|
<span className="sr-only">Reply all</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -91,7 +74,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<Forward className="h-4 w-4" />
|
<Forward className="h-4 w-4" />
|
||||||
<span className="sr-only">Forward</span>
|
<span className="sr-only">Forward</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -102,7 +85,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
<Separator orientation="vertical" className="mx-2 h-6" />
|
<Separator orientation="vertical" className="mx-2 h-6" />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
<Button variant="ghost" size="icon" disabled={true}>
|
||||||
<MoreVertical className="h-4 w-4" />
|
<MoreVertical className="h-4 w-4" />
|
||||||
<span className="sr-only">More</span>
|
<span className="sr-only">More</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -116,36 +99,14 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
{user ? (
|
{recipe ? (
|
||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<div className="flex items-start p-4">
|
{/* <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 && (
|
{recipe && recipe.LastChange && (
|
||||||
<div className="ml-auto text-xs text-muted-foreground">{format(new Date(recipe.LastChange), 'PPpp')}</div>
|
<div className="ml-auto text-xs text-muted-foreground">{format(new Date(recipe.LastChange), 'PPpp')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
<Separator />
|
|
||||||
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
|
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
|
||||||
{recipe && (
|
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold">Product Code: {recipe.productCode}</div>
|
<div className="font-semibold">Product Code: {recipe.productCode}</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -160,6 +121,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
|
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
|
||||||
</div>
|
</div>
|
||||||
|
<Separator />
|
||||||
<div>
|
<div>
|
||||||
<Accordion type="single" collapsible>
|
<Accordion type="single" collapsible>
|
||||||
{
|
{
|
||||||
|
|
@ -211,23 +173,6 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,173 +1,21 @@
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { type MaterialSetting, type Recipe01, type Recipes } from '@/models/recipe/schema'
|
import { type Recipes } from '@/models/recipe/schema'
|
||||||
import { memo, useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff, Server, Loader2 } from 'lucide-react'
|
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff, Server, Loader2 } from 'lucide-react'
|
||||||
import Nav from './nav'
|
import Nav from './nav'
|
||||||
import RecipeList from './recipe-list'
|
import RecipeList from './recipe-list'
|
||||||
import { format, isBefore, isToday } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import RecipeDisplay from './recipe-display'
|
import RecipeDisplay from './recipe-display'
|
||||||
import MaterialList from './material-list'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import taoAxios from '@/lib/taoAxios'
|
import taoAxios from '@/lib/taoAxios'
|
||||||
|
import type { ItemMetadata, ListMetadata } from '@/hooks/recipe-dashboard'
|
||||||
interface RecipeMenuProps {
|
import useRecipeDashboard, { EditorShowStateEnum } from '@/hooks/recipe-dashboard'
|
||||||
recipes: Recipes
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
recipe01: Recipe01[]
|
|
||||||
defaultSize?: number
|
|
||||||
isDevBranch: boolean
|
|
||||||
}
|
|
||||||
const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, defaultSize, isDevBranch }) => {
|
|
||||||
const [recipeList, setRecipeList] = useState<Recipe01[]>(recipe01)
|
|
||||||
|
|
||||||
const sortedRecipe01 = useMemo(() => {
|
|
||||||
return recipeList.sort((a, b) => (a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1))
|
|
||||||
}, [recipeList])
|
|
||||||
|
|
||||||
const { isPending, isSuccess, isError, mutate } = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
return taoAxios.post('/v2/recipes/', recipes, {
|
|
||||||
params: {
|
|
||||||
country_id: 'tha'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (search) {
|
|
||||||
const recipesFiltered = recipe01.filter(
|
|
||||||
item =>
|
|
||||||
item.productCode.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(item.name && item.name.toLowerCase().includes(search.toLowerCase()))
|
|
||||||
)
|
|
||||||
setRecipeList(recipesFiltered)
|
|
||||||
} else {
|
|
||||||
setRecipeList(recipe01)
|
|
||||||
}
|
|
||||||
}, [search])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ResizablePanel id="recipe-panel" 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">
|
|
||||||
<div className="pb-3 flex justify-end items-end">
|
|
||||||
<Button className="bg-primary text-white" onClick={() => mutate()}>
|
|
||||||
{isPending ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Loader2 size={20} className="animate-spin" />
|
|
||||||
<span>Updating...</span>
|
|
||||||
</div>
|
|
||||||
) : isSuccess ? (
|
|
||||||
'Updated'
|
|
||||||
) : isError ? (
|
|
||||||
'Error'
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Server size={20} />
|
|
||||||
Up Recipe to Server
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<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" value={search} onChange={e => setSearch(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<TabsContent value="all" className="m-0">
|
|
||||||
<RecipeList items={sortedRecipe01} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="today" className="m-0">
|
|
||||||
<RecipeList items={sortedRecipe01.filter(item => item.LastChange && isToday(item.LastChange))} />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle withHandle />
|
|
||||||
<ResizablePanel defaultSize={defaultSize}>
|
|
||||||
<RecipeDisplay recipes={recipes} />
|
|
||||||
</ResizablePanel>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
interface MaterialsProps {
|
|
||||||
recipes: Recipes
|
|
||||||
defaultSize?: number
|
|
||||||
isDevBranch: boolean
|
|
||||||
}
|
|
||||||
const Materials: React.FC<MaterialsProps> = memo(({ recipes, defaultSize, isDevBranch }) => {
|
|
||||||
const [materialSettingList, setMaterialSettingList] = useState<MaterialSetting[]>(recipes.MaterialSetting)
|
|
||||||
|
|
||||||
const sortedMaterialSettingList = useMemo(() => {
|
|
||||||
return materialSettingList.sort((a, b) => (a.id < b.id ? 1 : -1))
|
|
||||||
}, [materialSettingList])
|
|
||||||
|
|
||||||
const [search, setSearch] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (search) {
|
|
||||||
const materialSettingsFiltered = recipes.MaterialSetting.filter(
|
|
||||||
item =>
|
|
||||||
item.materialName.toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
item.id.toString().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
setMaterialSettingList(materialSettingsFiltered)
|
|
||||||
} else {
|
|
||||||
setMaterialSettingList(recipes.MaterialSetting)
|
|
||||||
}
|
|
||||||
}, [search])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ResizablePanel id="material-panel" defaultSize={defaultSize} minSize={30}>
|
|
||||||
<div className="flex items-center px-4 py-2">
|
|
||||||
<h1 className="text-xl font-bold">
|
|
||||||
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
|
|
||||||
</h1>
|
|
||||||
</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" value={search} onChange={e => setSearch(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<MaterialList items={sortedMaterialSettingList} />
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle withHandle />
|
|
||||||
<ResizablePanel defaultSize={defaultSize}>
|
|
||||||
<RecipeDisplay recipes={recipes} />
|
|
||||||
</ResizablePanel>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
interface RecipeEditorProps {
|
interface RecipeEditorProps {
|
||||||
isDevBranch: boolean
|
isDevBranch: boolean
|
||||||
|
|
@ -186,15 +34,118 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
|
||||||
|
|
||||||
const [showListIndex, setShowListIndex] = useState(0)
|
const [editorShowState, setEditorShowState] = useState(EditorShowStateEnum.RECIPES_IN_USE)
|
||||||
|
|
||||||
const { recipesEnable, recipeDisable } = useMemo(() => {
|
const { recipesEnable, recipesDisable } = useMemo<{
|
||||||
|
recipesEnable: ItemMetadata[]
|
||||||
|
recipesDisable: ItemMetadata[]
|
||||||
|
}>(() => {
|
||||||
return {
|
return {
|
||||||
recipesEnable: recipes.Recipe01.filter(r => r.isUse),
|
recipesEnable: recipes.Recipe01.filter(r => r.isUse).map(x => ({
|
||||||
recipeDisable: recipes.Recipe01.filter(r => !r.isUse)
|
id: x.productCode,
|
||||||
|
name: x.name,
|
||||||
|
lastChange: x.LastChange
|
||||||
|
})),
|
||||||
|
recipesDisable: recipes.Recipe01.filter(r => !r.isUse).map(x => ({
|
||||||
|
id: x.productCode,
|
||||||
|
name: x.name,
|
||||||
|
lastChange: x.LastChange
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}, [recipes])
|
}, [recipes])
|
||||||
|
|
||||||
|
const [currentItems, setCurrentItems] = useState<ItemMetadata[]>(recipesEnable)
|
||||||
|
|
||||||
|
const [listMetadata, setListMetadata] = useState<ListMetadata>({
|
||||||
|
items: recipesEnable,
|
||||||
|
currentSelectedId: undefined,
|
||||||
|
onSelectFn: id => setSelectedRecipe(id.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
const { selectedMaterial, setSelectedMaterial, selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
|
||||||
|
useShallow(state => ({
|
||||||
|
selectedMaterial: state.selectedMaterial,
|
||||||
|
setSelectedMaterial: state.setSelectedMaterial,
|
||||||
|
selectedRecipe: state.selectedRecipe,
|
||||||
|
setSelectedRecipe: state.setSelectedRecipe
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
// user click button from nav
|
||||||
|
useEffect(() => {
|
||||||
|
let list: ItemMetadata[] = []
|
||||||
|
let currentSelectId: string | number | undefined
|
||||||
|
let onSelectedFn: (id: string | number) => void = () => {}
|
||||||
|
|
||||||
|
if (editorShowState === EditorShowStateEnum.RECIPES_IN_USE) {
|
||||||
|
list = recipesEnable
|
||||||
|
currentSelectId = selectedRecipe
|
||||||
|
onSelectedFn = id => setSelectedRecipe(id.toString())
|
||||||
|
} else if (editorShowState === EditorShowStateEnum.RECIPES_NOT_IN_USE) {
|
||||||
|
list = recipesDisable
|
||||||
|
currentSelectId = selectedRecipe
|
||||||
|
onSelectedFn = id => setSelectedRecipe(id.toString())
|
||||||
|
} else if (editorShowState === EditorShowStateEnum.MATERIALS_SETTING) {
|
||||||
|
list = recipes.MaterialSetting.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
name: x.materialName
|
||||||
|
}))
|
||||||
|
currentSelectId = selectedMaterial
|
||||||
|
onSelectedFn = id => setSelectedMaterial(Number(id))
|
||||||
|
} else if (editorShowState === EditorShowStateEnum.TOPPING_GROUPS) {
|
||||||
|
list = recipes.Topping.ToppingGroup.map(x => ({
|
||||||
|
id: x.groupID,
|
||||||
|
name: x.name
|
||||||
|
}))
|
||||||
|
currentSelectId = selectedMaterial
|
||||||
|
onSelectedFn = id => setSelectedMaterial(Number(id))
|
||||||
|
} else if (editorShowState === EditorShowStateEnum.TOPPING_LIST) {
|
||||||
|
list = recipes.Topping.ToppingList.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
name: x.name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
setListMetadata({
|
||||||
|
items: list,
|
||||||
|
currentSelectedId: currentSelectId,
|
||||||
|
onSelectFn: onSelectedFn
|
||||||
|
})
|
||||||
|
setCurrentItems(list)
|
||||||
|
setSearch('')
|
||||||
|
}, [editorShowState])
|
||||||
|
|
||||||
|
const saveRecipeState = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
return taoAxios.post('/v2/recipes/', recipes, {
|
||||||
|
params: {
|
||||||
|
country_id: 'tha'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (search) {
|
||||||
|
const recipesFiltered = currentItems.filter(
|
||||||
|
item =>
|
||||||
|
item.id.toString().toLowerCase().includes(search.toLowerCase()) ||
|
||||||
|
(item.name && item.name.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
)
|
||||||
|
setListMetadata({
|
||||||
|
...listMetadata,
|
||||||
|
items: recipesFiltered
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setListMetadata({
|
||||||
|
...listMetadata,
|
||||||
|
items: currentItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [search])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
|
|
@ -228,19 +179,19 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||||
<Separator />
|
<Separator />
|
||||||
<Nav
|
<Nav
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
showListIndex={showListIndex}
|
showListIndex={editorShowState}
|
||||||
setShowListIndex={setShowListIndex}
|
setShowListIndex={setEditorShowState}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
index: 0,
|
index: EditorShowStateEnum.RECIPES_IN_USE,
|
||||||
title: 'Menu (Enabled)',
|
title: 'Menu (Enabled)',
|
||||||
label: recipesEnable.length.toString(),
|
label: recipesEnable.length.toString(),
|
||||||
icon: CupSoda
|
icon: CupSoda
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 1,
|
index: EditorShowStateEnum.RECIPES_NOT_IN_USE,
|
||||||
title: 'Menu (Disabled)',
|
title: 'Menu (Disabled)',
|
||||||
label: recipeDisable.length.toString(),
|
label: recipesDisable.length.toString(),
|
||||||
icon: WineOff
|
icon: WineOff
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
|
@ -248,23 +199,23 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||||
<Separator />
|
<Separator />
|
||||||
<Nav
|
<Nav
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
showListIndex={showListIndex}
|
showListIndex={editorShowState}
|
||||||
setShowListIndex={setShowListIndex}
|
setShowListIndex={setEditorShowState}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
index: 2,
|
index: EditorShowStateEnum.MATERIALS_SETTING,
|
||||||
title: 'Materials',
|
title: 'Materials',
|
||||||
label: recipes.MaterialSetting.length.toString(),
|
label: recipes.MaterialSetting.length.toString(),
|
||||||
icon: Wheat
|
icon: Wheat
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 3,
|
index: EditorShowStateEnum.TOPPING_GROUPS,
|
||||||
title: 'ToppingsGroups',
|
title: 'ToppingsGroups',
|
||||||
label: recipes.Topping.ToppingGroup.length.toString(),
|
label: recipes.Topping.ToppingGroup.length.toString(),
|
||||||
icon: Dessert
|
icon: Dessert
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 4,
|
index: EditorShowStateEnum.TOPPING_LIST,
|
||||||
title: 'ToppingsList',
|
title: 'ToppingsList',
|
||||||
label: recipes.Topping.ToppingList.length.toString(),
|
label: recipes.Topping.ToppingList.length.toString(),
|
||||||
icon: Cherry
|
icon: Cherry
|
||||||
|
|
@ -273,23 +224,52 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
{showListIndex === 0 ? (
|
<ResizablePanel id="recipe-panel" defaultSize={defaultLayout[1]} minSize={30}>
|
||||||
<RecipeMenu
|
<div className="flex items-center px-4 py-2">
|
||||||
recipes={recipes}
|
<h1 className="text-xl font-bold">
|
||||||
recipe01={recipesEnable}
|
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
|
||||||
defaultSize={defaultLayout[1]}
|
</h1>
|
||||||
isDevBranch={isDevBranch}
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="pb-3 flex justify-end items-end">
|
||||||
|
<Button className="bg-primary text-white" onClick={() => saveRecipeState.mutate()}>
|
||||||
|
{saveRecipeState.isPending ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
<span>Updating...</span>
|
||||||
|
</div>
|
||||||
|
) : saveRecipeState.isSuccess ? (
|
||||||
|
'Updated'
|
||||||
|
) : saveRecipeState.isError ? (
|
||||||
|
'Error'
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Server size={20} />
|
||||||
|
Up Recipe to Server
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<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" value={search} onChange={e => setSearch(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{listMetadata ? (
|
||||||
|
<RecipeList
|
||||||
|
items={listMetadata.items}
|
||||||
|
onSelect={listMetadata.onSelectFn}
|
||||||
|
currentSelectId={listMetadata.currentSelectedId}
|
||||||
/>
|
/>
|
||||||
) : showListIndex === 1 ? (
|
|
||||||
<RecipeMenu
|
|
||||||
recipes={recipes}
|
|
||||||
recipe01={recipeDisable}
|
|
||||||
defaultSize={defaultLayout[1]}
|
|
||||||
isDevBranch={isDevBranch}
|
|
||||||
/>
|
|
||||||
) : showListIndex === 2 ? (
|
|
||||||
<Materials recipes={recipes} defaultSize={defaultLayout[1]} isDevBranch={isDevBranch} />
|
|
||||||
) : null}
|
) : null}
|
||||||
|
</ResizablePanel>
|
||||||
|
<ResizableHandle withHandle />
|
||||||
|
<ResizablePanel defaultSize={defaultLayout[1]}>
|
||||||
|
<RecipeDisplay recipes={recipes} />
|
||||||
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,49 @@
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
import { type ItemMetadata } from '@/hooks/recipe-dashboard'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { type Recipe01 } from '@/models/recipe/schema'
|
|
||||||
import { formatDistanceToNow, isToday } from 'date-fns'
|
import { formatDistanceToNow, isToday } from 'date-fns'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
|
||||||
|
|
||||||
interface RecipeListProps {
|
interface RecipeListProps {
|
||||||
items: Recipe01[]
|
items: ItemMetadata[]
|
||||||
|
currentSelectId: string | number | undefined
|
||||||
|
onSelect: (id: string | number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
|
const RecipeList: React.FC<RecipeListProps> = ({ items, currentSelectId, onSelect }) => {
|
||||||
const { selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
|
|
||||||
useShallow(state => ({
|
|
||||||
selectedRecipe: state.selectedRecipe,
|
|
||||||
setSelectedRecipe: state.setSelectedRecipe
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-screen">
|
<ScrollArea className="h-screen">
|
||||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<button
|
<button
|
||||||
key={item.productCode}
|
key={item.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
|
'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'
|
currentSelectId === item.id && 'bg-muted'
|
||||||
)}
|
)}
|
||||||
onClick={() => setSelectedRecipe(item.productCode)}
|
onClick={() => onSelect(item.id)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full flex-col gap-1">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="text-md">{item.name}</div>
|
<div className="text-md">{item.name}</div>
|
||||||
{item.LastChange && isToday(item.LastChange) && (
|
{item.lastChange && isToday(item.lastChange) && (
|
||||||
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
|
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto text-xs',
|
'ml-auto text-xs',
|
||||||
selectedRecipe === item.productCode ? 'text-foreground' : 'text-muted-foreground'
|
currentSelectId === item.id ? 'text-foreground' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.LastChange &&
|
{item.lastChange &&
|
||||||
formatDistanceToNow(new Date(item.LastChange), {
|
formatDistanceToNow(new Date(item.lastChange), {
|
||||||
addSuffix: true
|
addSuffix: true
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{item.productCode}: {item.name || 'No Name'}
|
{item.id}: {item.name || 'No Name'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
|
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue