add recipe viewer
This commit is contained in:
parent
92b11f7b9d
commit
f7f1535695
31 changed files with 1532 additions and 151 deletions
|
|
@ -1,20 +0,0 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { RocketIcon } from '@radix-ui/react-icons'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>This is Home Page!!!!!!!</h1>
|
||||
<Button asChild>
|
||||
<Link to="/android">
|
||||
<RocketIcon className="mr-2 h-5 w-5" />
|
||||
Go to Android Page
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
|
|
@ -7,7 +7,7 @@ const LoginPage: React.FC = () => {
|
|||
const setUserInfo = userAuthStore(state => state.setUserInfo)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const redirectUrl = new URLSearchParams(window.location.search).get('redirect_to') ?? '/'
|
||||
const redirectUrl = new URLSearchParams(window.location.search).get('redirect_to') ?? '/recipes'
|
||||
|
||||
const loginWithGoogle = () => {
|
||||
// if is web mode then use window.open
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import useFileManager from '@/hooks/filemanager-android'
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import DataTable from './filemanager-table/data-table'
|
||||
import { FileIcon } from '@radix-ui/react-icons'
|
||||
import { type ColumnDef } from '@tanstack/react-table'
|
||||
import { LinuxFileType } from '@yume-chan/adb'
|
||||
import { formatDate } from 'date-fns'
|
||||
|
|
@ -11,6 +10,7 @@ import DataTableColumnHeader from './filemanager-table/data-table-column-header'
|
|||
import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { type AndroidFile } from '@/models/android/schema'
|
||||
import { File, Folder } from 'lucide-react'
|
||||
|
||||
export const FileManagerTab: React.FC = () => {
|
||||
const { currentPath, pushPath, scanPath } = useFileManager(
|
||||
|
|
@ -61,6 +61,13 @@ export const FileManagerTab: React.FC = () => {
|
|||
accessorKey: 'filename',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => {
|
||||
let icon: React.ReactNode
|
||||
if (row.original.type === LinuxFileType.File) {
|
||||
icon = <File size={20} strokeWidth={1} className="mr-2" />
|
||||
} else {
|
||||
icon = <Folder size={20} strokeWidth={1} className="mr-2" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex hover:cursor-pointer"
|
||||
|
|
@ -68,8 +75,8 @@ export const FileManagerTab: React.FC = () => {
|
|||
if (row.original.type !== LinuxFileType.File) pushPath(row.original.filename)
|
||||
}}
|
||||
>
|
||||
{row.original.type === LinuxFileType.File && <FileIcon className="mr-2 h-4 w-4" />}
|
||||
<span className={row.original.type !== LinuxFileType.File ? 'hover:underline' : 'ml-2'}>
|
||||
{icon}
|
||||
<span className={row.original.type !== LinuxFileType.File ? 'hover:underline' : ''}>
|
||||
{row.original.filename}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import useAndroidSwitcher from '@/hooks/android-switcher'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface AndroidSwitcherProps {
|
||||
isCollapsed?: boolean
|
||||
androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
}
|
||||
|
||||
const AndroidSwitcher: React.FC<AndroidSwitcherProps> = ({ androids, isCollapsed }) => {
|
||||
const { selectedAndroid, setSelectedAndroid } = useAndroidSwitcher(
|
||||
useShallow(state => ({
|
||||
selectedAndroid: state.selectedAndroid,
|
||||
setSelectedAndroid: state.setSelectedAndroid
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<Select defaultValue={selectedAndroid} onValueChange={setSelectedAndroid}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
||||
isCollapsed && 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
||||
)}
|
||||
aria-label="Select android"
|
||||
>
|
||||
<SelectValue placeholder="Select an android">
|
||||
{androids.find(android => android.serial === selectedAndroid)?.icon}
|
||||
<span className={cn('ml-2', isCollapsed && 'hidden')}>
|
||||
{androids.find(android => android.serial === selectedAndroid)?.label}
|
||||
</span>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{androids.map(android => (
|
||||
<SelectItem key={android.serial} value={android.serial}>
|
||||
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
|
||||
{android.icon}
|
||||
{android.deviceName}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
export default AndroidSwitcher
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type LucideIcon } from 'lucide-react'
|
||||
|
||||
interface NavProps {
|
||||
isCollapsed: boolean
|
||||
links: {
|
||||
index: number
|
||||
title: string
|
||||
label?: string
|
||||
icon: LucideIcon
|
||||
}[]
|
||||
showListIndex: number
|
||||
setShowListIndex: (index: number) => void
|
||||
}
|
||||
|
||||
const Nav: React.FC<NavProps> = ({ links, isCollapsed, showListIndex, setShowListIndex }) => {
|
||||
return (
|
||||
<div data-collapsed={isCollapsed} className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2">
|
||||
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||
{links.map((link, index) =>
|
||||
isCollapsed ? (
|
||||
<Tooltip key={index} delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
key={index}
|
||||
variant={showListIndex === link.index ? 'default' : 'ghost'}
|
||||
size={'icon'}
|
||||
className={cn(
|
||||
'h-9 w-9',
|
||||
showListIndex === link.index &&
|
||||
'dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white'
|
||||
)}
|
||||
onClick={() => setShowListIndex(link.index)}
|
||||
>
|
||||
<link.icon className="h-4 w-4" />
|
||||
<span className="sr-only">{link.title}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex items-center gap-4">
|
||||
{link.title}
|
||||
{link.label && <span className="ml-auto text-muted-foreground">{link.label}</span>}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
key={index}
|
||||
variant={showListIndex === link.index ? 'default' : 'ghost'}
|
||||
size={'sm'}
|
||||
className={cn(
|
||||
showListIndex === link.index &&
|
||||
'dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white',
|
||||
'justify-start'
|
||||
)}
|
||||
onClick={() => setShowListIndex(link.index)}
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{link.title}
|
||||
{link.label && (
|
||||
<span className={cn('ml-auto', showListIndex === link.index && 'text-background dark:text-white')}>
|
||||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nav
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import { type Recipes } from '@/models/recipe/schema'
|
||||
import { format } from 'date-fns'
|
||||
import { Archive, ArchiveX, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface RecipeDisplayProps {
|
||||
recipes: Recipes
|
||||
}
|
||||
|
||||
const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||
const selectedRecipe = useRecipeDashboard(state => state.selectedRecipe)
|
||||
|
||||
const recipe = useMemo(() => {
|
||||
return recipes.Recipe01.find(recipe => recipe.productCode === selectedRecipe)
|
||||
}, [selectedRecipe])
|
||||
|
||||
const user:
|
||||
| {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
| undefined = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john_doe@gmail.com'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Archive className="h-4 w-4" />
|
||||
<span className="sr-only">Archive</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Archive</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<ArchiveX className="h-4 w-4" />
|
||||
<span className="sr-only">Move to junk</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to junk</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">Move to trash</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Move to trash</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="mx-1 h-6" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Reply className="h-4 w-4" />
|
||||
<span className="sr-only">Reply</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<ReplyAll className="h-4 w-4" />
|
||||
<span className="sr-only">Reply all</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reply all</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<Forward className="h-4 w-4" />
|
||||
<span className="sr-only">Forward</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Forward</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-2 h-6" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" disabled={!recipe}>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
|
||||
<DropdownMenuItem>Star thread</DropdownMenuItem>
|
||||
<DropdownMenuItem>Add label</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mute thread</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Separator />
|
||||
{user ? (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex items-start p-4">
|
||||
<div className="flex items-start gap-4 text-sm">
|
||||
<Avatar>
|
||||
<AvatarImage alt={user.name} />
|
||||
<AvatarFallback>
|
||||
{user.name
|
||||
.split(' ')
|
||||
.map(chunk => chunk[0])
|
||||
.join('')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<div className="font-semibold">{user.name}</div>
|
||||
<div className="line-clamp-1 text-xs">
|
||||
<span className="font-medium">ID:</span> {user.id}
|
||||
</div>
|
||||
<div className="line-clamp-1 text-xs">
|
||||
<span className="font-medium">Email:</span> {user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{recipe && recipe.LastChange && (
|
||||
<div className="ml-auto text-xs text-muted-foreground">{format(new Date(recipe.LastChange), 'PPpp')}</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
|
||||
{/* show name and productCode of recipe */}
|
||||
{recipe && (
|
||||
<div>
|
||||
<div className="font-semibold">{recipe.name}</div>
|
||||
<div className="text-xs">{recipe.productCode}</div>
|
||||
</div>
|
||||
)}
|
||||
{/* show recipe description */}
|
||||
{recipe && <div className="mt-4">{recipe.Description}</div>}
|
||||
</div>
|
||||
<Separator className="mt-auto" />
|
||||
<div className="p-4">
|
||||
<form>
|
||||
<div className="grid gap-4">
|
||||
<Textarea className="p-4" placeholder={`Reply ${`John Doe`}...`} />
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="mute" className="flex items-center gap-2 text-xs font-normal">
|
||||
<Switch id="mute" aria-label="Mute thread" /> Mute this thread
|
||||
</Label>
|
||||
<Button onClick={e => e.preventDefault()} size="sm" className="ml-auto">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">No message selected</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeDisplay
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
import { Input } from '@/components/ui/input'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type Recipe01, type Recipes } from '@/models/recipe/schema'
|
||||
import { memo, useMemo, useState } from 'react'
|
||||
import AndroidSwitcher from './android-switcher'
|
||||
import { Search, CupSoda, Wheat, Dessert, Cherry, WineOff } from 'lucide-react'
|
||||
import Nav from './nav'
|
||||
import RecipeList from './recipe-list'
|
||||
import { isBefore, isToday } from 'date-fns'
|
||||
import RecipeDisplay from './recipe-display'
|
||||
|
||||
interface RecipeMenuProps {
|
||||
recipes: Recipes
|
||||
recipe01: Recipe01[]
|
||||
defaultSize?: number
|
||||
isDevBranch: boolean
|
||||
}
|
||||
const RecipeMenu: React.FC<RecipeMenuProps> = memo(({ recipes, recipe01, defaultSize, isDevBranch }) => {
|
||||
recipe01 = useMemo(() => {
|
||||
return recipe01.sort((a, b) => (a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1))
|
||||
}, [recipe01])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResizablePanel defaultSize={defaultSize} minSize={30}>
|
||||
<Tabs defaultValue="all">
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<h1 className="text-xl font-bold">
|
||||
Recipe Version: {recipes.MachineSetting.configNumber} {isDevBranch ? '(Dev)' : ''}
|
||||
</h1>
|
||||
<TabsList className="ml-auto">
|
||||
<TabsTrigger value="all" className="text-zinc-600 dark:text-zinc-200">
|
||||
All Menu
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="today" className="text-zinc-600 dark:text-zinc-200">
|
||||
Today
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search" className="pl-8" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<TabsContent value="all" className="m-0">
|
||||
<RecipeList items={recipe01} />
|
||||
</TabsContent>
|
||||
<TabsContent value="today" className="m-0">
|
||||
<RecipeList items={recipe01.filter(item => item.LastChange && isToday(item.LastChange))} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={defaultSize}>
|
||||
<RecipeDisplay recipes={recipes} />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
interface RecipeEditorProps {
|
||||
androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
isDevBranch: boolean
|
||||
recipes: Recipes
|
||||
defaultLayout: number[] | undefined
|
||||
defaultCollapsed?: boolean
|
||||
navCollapsedSize: number
|
||||
}
|
||||
|
||||
export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||
androids,
|
||||
recipes,
|
||||
isDevBranch,
|
||||
defaultLayout = [265, 440, 655],
|
||||
defaultCollapsed = false,
|
||||
navCollapsedSize
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed)
|
||||
|
||||
const [showListIndex, setShowListIndex] = useState(0)
|
||||
|
||||
const { recipesEnable, recipeDisable } = useMemo(() => {
|
||||
return {
|
||||
recipesEnable: recipes.Recipe01.filter(r => r.isUse),
|
||||
recipeDisable: recipes.Recipe01.filter(r => !r.isUse)
|
||||
}
|
||||
}, [recipes])
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
onLayout={(sizes: number[]) => {
|
||||
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
||||
}}
|
||||
className="h-full max-h-[800px] items-stretch"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={defaultLayout[0]}
|
||||
collapsedSize={navCollapsedSize}
|
||||
collapsible={true}
|
||||
minSize={15}
|
||||
maxSize={20}
|
||||
onCollapse={() => {
|
||||
setIsCollapsed(true)
|
||||
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`
|
||||
}}
|
||||
onExpand={() => {
|
||||
setIsCollapsed(false)
|
||||
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`
|
||||
}}
|
||||
className={cn(isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out')}
|
||||
>
|
||||
<div className={cn('flex h-[52px] items-center justify-center', isCollapsed ? 'h-[52px]' : 'px-2')}>
|
||||
<AndroidSwitcher isCollapsed={isCollapsed} androids={androids} />
|
||||
</div>
|
||||
<Separator />
|
||||
<Nav
|
||||
isCollapsed={isCollapsed}
|
||||
showListIndex={showListIndex}
|
||||
setShowListIndex={setShowListIndex}
|
||||
links={[
|
||||
{
|
||||
index: 0,
|
||||
title: 'Menu (Enabled)',
|
||||
label: recipesEnable.length.toString(),
|
||||
icon: CupSoda
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
title: 'Menu (Disabled)',
|
||||
label: recipeDisable.length.toString(),
|
||||
icon: WineOff
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Separator />
|
||||
<Nav
|
||||
isCollapsed={isCollapsed}
|
||||
showListIndex={showListIndex}
|
||||
setShowListIndex={setShowListIndex}
|
||||
links={[
|
||||
{
|
||||
index: 2,
|
||||
title: 'Materials',
|
||||
label: recipes.MaterialSetting.length.toString(),
|
||||
icon: Wheat
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
title: 'ToppingsGroups',
|
||||
label: recipes.Topping.ToppingGroup.length.toString(),
|
||||
icon: Dessert
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
title: 'ToppingsList',
|
||||
label: recipes.Topping.ToppingList.length.toString(),
|
||||
icon: Cherry
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
{showListIndex === 0 ? (
|
||||
<RecipeMenu
|
||||
recipes={recipes}
|
||||
recipe01={recipesEnable}
|
||||
defaultSize={defaultLayout[1]}
|
||||
isDevBranch={isDevBranch}
|
||||
/>
|
||||
) : showListIndex === 1 ? (
|
||||
<RecipeMenu
|
||||
recipes={recipes}
|
||||
recipe01={recipeDisable}
|
||||
defaultSize={defaultLayout[1]}
|
||||
isDevBranch={isDevBranch}
|
||||
/>
|
||||
) : null}
|
||||
</ResizablePanelGroup>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeEditor
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type Recipe01 } from '@/models/recipe/schema'
|
||||
import { formatDistanceToNow, isToday } from 'date-fns'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface RecipeListProps {
|
||||
items: Recipe01[]
|
||||
}
|
||||
|
||||
const RecipeList: React.FC<RecipeListProps> = ({ items }) => {
|
||||
const { selectedRecipe, setSelectedRecipe } = useRecipeDashboard(
|
||||
useShallow(state => ({
|
||||
selectedRecipe: state.selectedRecipe,
|
||||
setSelectedRecipe: state.setSelectedRecipe
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-screen">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.productCode}
|
||||
className={cn(
|
||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
|
||||
selectedRecipe === item.productCode && 'bg-muted'
|
||||
)}
|
||||
onClick={() => setSelectedRecipe(item.productCode)}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold">{item.name}</div>
|
||||
{item.LastChange && isToday(item.LastChange) && (
|
||||
<span className="flex h-2 w-2 rounded-full bg-blue-600" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'ml-auto text-xs',
|
||||
selectedRecipe === item.productCode ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{item.LastChange &&
|
||||
formatDistanceToNow(new Date(item.LastChange), {
|
||||
addSuffix: true
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-medium">{`${item.productCode}: ${item.name}`}</div>
|
||||
</div>
|
||||
{/* <div className="line-clamp-2 text-xs text-muted-foreground">{item.substring(0, 300)}</div>
|
||||
{item.labels.length ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.labels.map(label => (
|
||||
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
|
||||
{label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null} */}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeList
|
||||
|
|
@ -1,27 +1,104 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { columns } from './components/recipe-table-components/columns'
|
||||
import DataTable from './components/recipe-table-components/data-table'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
import RecipeEditor from './components/recipe-editor-components/recipe-editor'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { Smartphone } from 'lucide-react'
|
||||
import { type Recipes } from '@/models/recipe/schema'
|
||||
import useFileManager from '@/hooks/filemanager-android'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import useAdb from '@/hooks/useAdb'
|
||||
import { isBefore } from 'date-fns'
|
||||
|
||||
const androids: {
|
||||
label: string
|
||||
deviceName: string
|
||||
serial: string
|
||||
icon: React.ReactNode
|
||||
}[] = [
|
||||
{
|
||||
label: 'Test Device',
|
||||
deviceName: 'Test Device',
|
||||
serial: 'Test Device',
|
||||
icon: <Smartphone />
|
||||
}
|
||||
]
|
||||
|
||||
const RecipesTablePage = () => {
|
||||
const recipeQuery = useLocalStorage(state => state.recipeQuery)
|
||||
const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
|
||||
// const recipeQuery = useLocalStorage(state => state.recipeQuery)
|
||||
// const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
|
||||
|
||||
const { data: recipeDashboardList, isLoading } = useQuery({
|
||||
queryKey: ['recipe-overview'],
|
||||
queryFn: () => getRecipesDashboard(recipeQuery)
|
||||
})
|
||||
// const { data: recipeDashboardList, isLoading } = useQuery({
|
||||
// queryKey: ['recipe-overview'],
|
||||
// queryFn: () => getRecipesDashboard(recipeQuery)
|
||||
// })
|
||||
|
||||
const { layout, collapsed } = useLocalStorage(
|
||||
useShallow(state => ({
|
||||
layout: state.layout,
|
||||
collapsed: state.collapsed
|
||||
}))
|
||||
)
|
||||
|
||||
const adb = useAdb(state => state.adb)
|
||||
|
||||
const readJsonFile = useFileManager(state => state.readJsonFile)
|
||||
|
||||
const [recipes, setRecipes] = useState<Recipes>()
|
||||
|
||||
const [isDevBranch, setIsDevBranch] = useState(false)
|
||||
|
||||
const readData = useCallback(async () => {
|
||||
try {
|
||||
return await readJsonFile<Recipes>('/sdcard/coffeevending/cfg/recipe_branch_dev.json').then(data => {
|
||||
if (data) {
|
||||
setIsDevBranch(true)
|
||||
data.Recipe01 = data.Recipe01.sort((a, b) =>
|
||||
a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1
|
||||
)
|
||||
}
|
||||
return data
|
||||
})
|
||||
} catch (e) {
|
||||
return await readJsonFile<Recipes>('/sdcard/coffeevending/coffeethai02.json').then(data => {
|
||||
if (data) {
|
||||
setIsDevBranch(false)
|
||||
data.Recipe01 = data.Recipe01.sort((a, b) =>
|
||||
a.LastChange && b.LastChange && isBefore(a.LastChange, b.LastChange) ? 1 : -1
|
||||
)
|
||||
}
|
||||
return data
|
||||
})
|
||||
}
|
||||
}, [adb])
|
||||
|
||||
useEffect(() => {
|
||||
readData().then(data => {
|
||||
setRecipes(data)
|
||||
})
|
||||
}, [readData])
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<section>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
</section>
|
||||
<section>
|
||||
<DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
|
||||
</section>
|
||||
</div>
|
||||
// <div className="flex w-full flex-col gap-3">
|
||||
// <section>
|
||||
// <h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
// </section>
|
||||
// <section>
|
||||
// <DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
|
||||
// </section>
|
||||
// </div>
|
||||
<>
|
||||
{recipes ? (
|
||||
<RecipeEditor
|
||||
androids={androids}
|
||||
defaultLayout={layout}
|
||||
navCollapsedSize={4}
|
||||
recipes={recipes}
|
||||
isDevBranch={isDevBranch}
|
||||
defaultCollapsed={collapsed}
|
||||
/>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
const SelectCountryPage: React.FC = () => {
|
||||
const { recipeQuery, setRecipeQuery } = useLocalStorage(
|
||||
useShallow(state => ({
|
||||
recipeQuery: state.recipeQuery,
|
||||
setRecipeQuery: state.setRecipeQuery
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>SelectContryPage</h1>
|
||||
<Button variant={'link'} onClick={() => setRecipeQuery({ countryID: 'tha', filename: 'coffeethai02_635.json' })}>
|
||||
Thai
|
||||
</Button>
|
||||
{recipeQuery && <Link to={`/recipes/${recipeQuery?.countryID}/${recipeQuery?.filename}`}>Recipes</Link>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectCountryPage
|
||||
Loading…
Add table
Add a link
Reference in a new issue