Update: Something spicy for recipe viewing only but called it editor lol

This commit is contained in:
Kenta420 2024-03-19 17:19:05 +07:00
parent 72187f348b
commit 7dd075dd59
8 changed files with 450 additions and 346 deletions

View file

@ -12,6 +12,26 @@ interface materialDashboard {
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 {
selectedRecipe: string
setSelectedRecipe: (recipe: string) => void

View file

@ -1,12 +1,13 @@
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { type EditorShowStateEnum } from '@/hooks/recipe-dashboard'
import { cn } from '@/lib/utils'
import { type LucideIcon } from 'lucide-react'
interface NavProps {
isCollapsed: boolean
links: {
index: number
index: EditorShowStateEnum
title: string
label?: string
icon: LucideIcon

View file

@ -1,16 +1,11 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
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 { 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 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'
@ -25,25 +20,13 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
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 overflow-y-auto">
<div className="flex items-center p-2">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<Archive className="h-4 w-4" />
<span className="sr-only">Archive</span>
</Button>
@ -52,7 +35,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<ArchiveX className="h-4 w-4" />
<span className="sr-only">Move to junk</span>
</Button>
@ -61,7 +44,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Move to trash</span>
</Button>
@ -73,7 +56,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
<div className="ml-auto flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<Reply className="h-4 w-4" />
<span className="sr-only">Reply</span>
</Button>
@ -82,7 +65,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<ReplyAll className="h-4 w-4" />
<span className="sr-only">Reply all</span>
</Button>
@ -91,7 +74,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<Forward className="h-4 w-4" />
<span className="sr-only">Forward</span>
</Button>
@ -102,7 +85,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
<Separator orientation="vertical" className="mx-2 h-6" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" disabled={!recipe}>
<Button variant="ghost" size="icon" disabled={true}>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More</span>
</Button>
@ -116,118 +99,80 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
</DropdownMenu>
</div>
<Separator />
{user ? (
{recipe ? (
<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>
{/* <div className="flex items-start p-4">
{recipe && recipe.LastChange && (
<div className="ml-auto text-xs text-muted-foreground">{format(new Date(recipe.LastChange), 'PPpp')}</div>
)}
</div>
<Separator />
</div> */}
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
{recipe && (
<div>
<div className="font-semibold">Product Code: {recipe.productCode}</div>
<div>
<div className="font-semibold">Product Code: {recipe.productCode}</div>
<div>
<span className="font-semibold">Name:</span> {recipe.name}
</div>
<div>
<span className="font-semibold">Other Name:</span> {recipe.otherName}
</div>
<div>
<span className="font-semibold">Description:</span> {recipe.Description}
</div>
<div>
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
</div>
<div>
<Accordion type="single" collapsible>
{
// list all recipes
recipe.recipes
.filter(r => r.isUse)
.map((recipe, index) => (
<AccordionItem key={index} value={'item-' + index}>
<AccordionTrigger>{recipe.materialPathId}</AccordionTrigger>
<AccordionContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Is Use</TableHead>
<TableHead>Powder Gram</TableHead>
<TableHead>Powder Time</TableHead>
<TableHead>Syrup Gram</TableHead>
<TableHead>Syrup Time</TableHead>
<TableHead>Hot Water</TableHead>
<TableHead>Cold Water</TableHead>
<TableHead>Mix Order</TableHead>
<TableHead>String Param</TableHead>
<TableHead>Stir Time</TableHead>
<TableHead>Feed param</TableHead>
<TableHead>Feed pattern</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">{JSON.stringify(recipe.isUse)}</TableCell>
<TableCell>{recipe.powderGram}</TableCell>
<TableCell>{recipe.powderTime}</TableCell>
<TableCell>{recipe.syrupGram}</TableCell>
<TableCell>{recipe.syrupTime}</TableCell>
<TableCell>{recipe.waterYield}</TableCell>
<TableCell>{recipe.waterCold}</TableCell>
<TableCell>{recipe.MixOrder}</TableCell>
<TableCell>{recipe.StringParam}</TableCell>
<TableCell>{recipe.stirTime}</TableCell>
<TableCell>{recipe.FeedParameter}</TableCell>
<TableCell>{recipe.FeedPattern}</TableCell>
</TableRow>
</TableBody>
</Table>
</AccordionContent>
</AccordionItem>
))
}
</Accordion>
</div>
<span className="font-semibold">Name:</span> {recipe.name}
</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>
<span className="font-semibold">Other Name:</span> {recipe.otherName}
</div>
</form>
<div>
<span className="font-semibold">Description:</span> {recipe.Description}
</div>
<div>
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
</div>
<Separator />
<div>
<Accordion type="single" collapsible>
{
// list all recipes
recipe.recipes
.filter(r => r.isUse)
.map((recipe, index) => (
<AccordionItem key={index} value={'item-' + index}>
<AccordionTrigger>{recipe.materialPathId}</AccordionTrigger>
<AccordionContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Is Use</TableHead>
<TableHead>Powder Gram</TableHead>
<TableHead>Powder Time</TableHead>
<TableHead>Syrup Gram</TableHead>
<TableHead>Syrup Time</TableHead>
<TableHead>Hot Water</TableHead>
<TableHead>Cold Water</TableHead>
<TableHead>Mix Order</TableHead>
<TableHead>String Param</TableHead>
<TableHead>Stir Time</TableHead>
<TableHead>Feed param</TableHead>
<TableHead>Feed pattern</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">{JSON.stringify(recipe.isUse)}</TableCell>
<TableCell>{recipe.powderGram}</TableCell>
<TableCell>{recipe.powderTime}</TableCell>
<TableCell>{recipe.syrupGram}</TableCell>
<TableCell>{recipe.syrupTime}</TableCell>
<TableCell>{recipe.waterYield}</TableCell>
<TableCell>{recipe.waterCold}</TableCell>
<TableCell>{recipe.MixOrder}</TableCell>
<TableCell>{recipe.StringParam}</TableCell>
<TableCell>{recipe.stirTime}</TableCell>
<TableCell>{recipe.FeedParameter}</TableCell>
<TableCell>{recipe.FeedPattern}</TableCell>
</TableRow>
</TableBody>
</Table>
</AccordionContent>
</AccordionItem>
))
}
</Accordion>
</div>
</div>
</div>
</div>
) : (

View file

@ -1,173 +1,21 @@
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 MaterialSetting, type Recipe01, type Recipes } from '@/models/recipe/schema'
import { memo, useEffect, useMemo, useState } from 'react'
import { type Recipes } from '@/models/recipe/schema'
import { useEffect, useMemo, useState } from 'react'
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff, Server, Loader2 } from 'lucide-react'
import Nav from './nav'
import RecipeList from './recipe-list'
import { format, isBefore, isToday } from 'date-fns'
import { format } from 'date-fns'
import RecipeDisplay from './recipe-display'
import MaterialList from './material-list'
import { Button } from '@/components/ui/button'
import { useMutation } from '@tanstack/react-query'
import taoAxios from '@/lib/taoAxios'
interface RecipeMenuProps {
recipes: Recipes
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>
</>
)
})
import type { ItemMetadata, ListMetadata } from '@/hooks/recipe-dashboard'
import useRecipeDashboard, { EditorShowStateEnum } from '@/hooks/recipe-dashboard'
import { useShallow } from 'zustand/react/shallow'
interface RecipeEditorProps {
isDevBranch: boolean
@ -186,15 +34,118 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
}) => {
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 {
recipesEnable: recipes.Recipe01.filter(r => r.isUse),
recipeDisable: recipes.Recipe01.filter(r => !r.isUse)
recipesEnable: recipes.Recipe01.filter(r => r.isUse).map(x => ({
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])
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 (
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
@ -228,19 +179,19 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
<Separator />
<Nav
isCollapsed={isCollapsed}
showListIndex={showListIndex}
setShowListIndex={setShowListIndex}
showListIndex={editorShowState}
setShowListIndex={setEditorShowState}
links={[
{
index: 0,
index: EditorShowStateEnum.RECIPES_IN_USE,
title: 'Menu (Enabled)',
label: recipesEnable.length.toString(),
icon: CupSoda
},
{
index: 1,
index: EditorShowStateEnum.RECIPES_NOT_IN_USE,
title: 'Menu (Disabled)',
label: recipeDisable.length.toString(),
label: recipesDisable.length.toString(),
icon: WineOff
}
]}
@ -248,23 +199,23 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
<Separator />
<Nav
isCollapsed={isCollapsed}
showListIndex={showListIndex}
setShowListIndex={setShowListIndex}
showListIndex={editorShowState}
setShowListIndex={setEditorShowState}
links={[
{
index: 2,
index: EditorShowStateEnum.MATERIALS_SETTING,
title: 'Materials',
label: recipes.MaterialSetting.length.toString(),
icon: Wheat
},
{
index: 3,
index: EditorShowStateEnum.TOPPING_GROUPS,
title: 'ToppingsGroups',
label: recipes.Topping.ToppingGroup.length.toString(),
icon: Dessert
},
{
index: 4,
index: EditorShowStateEnum.TOPPING_LIST,
title: 'ToppingsList',
label: recipes.Topping.ToppingList.length.toString(),
icon: Cherry
@ -273,23 +224,52 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
/>
</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}
/>
) : showListIndex === 2 ? (
<Materials recipes={recipes} defaultSize={defaultLayout[1]} isDevBranch={isDevBranch} />
) : null}
<ResizablePanel id="recipe-panel" defaultSize={defaultLayout[1]} 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">
<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}
/>
) : null}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[1]}>
<RecipeDisplay recipes={recipes} />
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
)

View file

@ -1,56 +1,49 @@
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 { type Recipe01 } from '@/models/recipe/schema'
import { formatDistanceToNow, isToday } from 'date-fns'
import { useShallow } from 'zustand/react/shallow'
interface RecipeListProps {
items: Recipe01[]
items: ItemMetadata[]
currentSelectId: string | number | undefined
onSelect: (id: string | number) => void
}
const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
const { selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
useShallow(state => ({
selectedRecipe: state.selectedRecipe,
setSelectedRecipe: state.setSelectedRecipe
}))
)
const RecipeList: React.FC<RecipeListProps> = ({ items, currentSelectId, onSelect }) => {
return (
<ScrollArea className="h-screen">
<div className="flex flex-col gap-2 p-4 pt-0">
{items.map(item => (
<button
key={item.productCode}
key={item.id}
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'
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 items-center">
<div className="flex items-center gap-2">
<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" />
)}
</div>
<div
className={cn(
'ml-auto text-xs',
selectedRecipe === item.productCode ? 'text-foreground' : 'text-muted-foreground'
currentSelectId === item.id ? 'text-foreground' : 'text-muted-foreground'
)}
>
{item.LastChange &&
formatDistanceToNow(new Date(item.LastChange), {
{item.lastChange &&
formatDistanceToNow(new Date(item.lastChange), {
addSuffix: true
})}
</div>
</div>
<div className="text-xs">
{item.productCode}: {item.name || 'No Name'}
{item.id}: {item.name || 'No Name'}
</div>
</div>
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>