test upload file to server

This commit is contained in:
Kenta420 2024-02-05 11:45:54 +07:00
parent 16e0e4f9d8
commit aaa60216b2
43 changed files with 1814 additions and 285 deletions

View file

@ -1,17 +1,43 @@
import { createBrowserRouter, createHashRouter, RouterProvider, type RouteObject } from 'react-router-dom'
import AuthCallBack from './AuthCallBack'
import MainLayout from './layouts/MainLayout'
import HomePage from './pages/Home'
import LoginPage from './pages/Login'
import AndroidPage from './pages/Android'
import HomePage from './pages/home'
import LoginPage from './pages/login'
import AndroidPage from './pages/android'
import RecipesPage from './pages/recipes/recipes'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import UploadPage from './pages/upload'
import { type MenuList } from './components/sidebar'
import { DashboardIcon, FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
const sideBarMenuList: MenuList = [
{
title: 'Home',
icon: DashboardIcon,
link: '/'
},
{
title: 'Recipes',
icon: FileTextIcon,
link: '/recipes'
},
{
title: 'Android',
icon: RocketIcon,
link: '/android'
},
{
title: 'Upload',
icon: UploadIcon,
link: '/upload'
}
]
function router() {
const routes: RouteObject[] = [
{
path: '/',
element: <MainLayout />,
element: <MainLayout sidebarMenu={sideBarMenuList} />,
children: [
{
path: '/',
@ -24,6 +50,10 @@ function router() {
{
path: 'android',
element: <AndroidPage />
},
{
path: 'upload',
element: <UploadPage />
}
]
},

View file

@ -5,23 +5,27 @@ const AuthCallBack: React.FC = () => {
const params = new URLSearchParams(window.location.search)
// emit message to main process
window.opener.postMessage(
{
payload: 'loginSuccess',
data: {
id: params.get('id'),
name: params.get('name'),
email: params.get('email'),
picture: params.get('picture'),
permissions: params.get('permissions')
}
},
'*'
)
if (params.get('kind') === 'electron') {
window.location.href = import.meta.env.TAOBIN_RECIPE_MANAGER_DEEPLINK_PROTOCOL + '/login' + window.location.search
} else {
window.opener.postMessage(
{
payload: 'loginSuccess',
data: {
id: params.get('id'),
name: params.get('name'),
email: params.get('email'),
picture: params.get('picture'),
permissions: params.get('permissions')
}
},
'*'
)
window.close()
window.close()
}
return <div>it just call back</div>
return <div>Login Success. You can close this window now.</div>
}
export default AuthCallBack

View file

@ -1,29 +1,32 @@
import { DashboardIcon, FileTextIcon, RocketIcon } from '@radix-ui/react-icons'
import React from 'react'
import { Link } from 'react-router-dom'
const Sidebar: React.FC = () => {
export type MenuList = {
title: string
icon: React.ComponentType<{ className?: string }>
link: string
}[]
interface SideBarProps {
menuList: MenuList
}
const Sidebar: React.FC<SideBarProps> = ({ menuList }) => {
return (
<aside className="fixed top-0 left-0 z-40 w-64 pt-20 h-screen">
<div className="h-full px-3 pb-4 overflow-y-auto bg-zinc-700">
<ul className="space-y-2 font-medium">
<li>
<Link to="/" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500">
<DashboardIcon className="inline-block w-6 h-6 mr-3" />
Dashboard
</Link>
</li>
<li>
<Link to="/recipes" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500">
<FileTextIcon className="inline-block w-6 h-6 mr-3" />
Recipes
</Link>
</li>
<li>
<Link to="/android" className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500">
<RocketIcon className="inline-block w-6 h-6 mr-3" />
Android
</Link>
</li>
{menuList.map((item, index) => (
<li key={index}>
<Link
to={item.link}
className="flex items-center px-3 py-2 text-sm text-white rounded-md hover:bg-zinc-500"
>
{<item.icon className="inline-block w-6 h-6 mr-3" />}
{item.title}
</Link>
</li>
))}
</ul>
</div>
</aside>

View file

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -0,0 +1,71 @@
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import React, { type ChangeEvent, useRef } from 'react'
interface DropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'> {
classNameWrapper?: string
className?: string
dropMessage: string
handleOnDrop: (acceptedFiles: FileList | null) => void
}
const Dropzone = React.forwardRef<HTMLDivElement, DropzoneProps>(
({ className, classNameWrapper, dropMessage, handleOnDrop, ...props }, ref) => {
const inputRef = useRef<HTMLInputElement | null>(null)
// Function to handle drag over event
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
handleOnDrop(null)
}
// Function to handle drop event
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
const { files } = e.dataTransfer
if (inputRef.current) {
inputRef.current.files = files
handleOnDrop(files)
}
}
// Function to simulate a click on the file input element
const handleButtonClick = () => {
if (inputRef.current) {
inputRef.current.click()
}
}
return (
<Card
ref={ref}
className={cn(
`border-2 border-dashed bg-muted hover:cursor-pointer hover:border-muted-foreground/50`,
classNameWrapper
)}
>
<CardContent
className="flex flex-col items-center justify-center space-y-2 px-2 py-4 text-xs"
onDragOver={handleDragOver}
onDrop={handleDrop}
onClick={handleButtonClick}
>
<div className="flex items-center justify-center text-muted-foreground">
<span className="font-medium">{dropMessage}</span>
<Input
{...props}
value={undefined}
ref={inputRef}
type="file"
className={cn('hidden', className)}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleOnDrop(e.target.files)}
/>
</div>
</CardContent>
</Card>
)
}
)
export default Dropzone

View file

@ -0,0 +1,15 @@
export enum PermissionEnum {
THAI_PERMISSIONS = 1,
MALAY_PERMISSIONS = 1 << 1,
AUS_PERMISSIONS = 1 << 2,
ALPHA3_PERMISSIONS = 1 << 3,
VIEWER_PERMISSIONS = 1 << 4,
EDITOR_PERMISSIONS = VIEWER_PERMISSIONS << 1,
SUPER_ADMIN = THAI_PERMISSIONS |
MALAY_PERMISSIONS |
AUS_PERMISSIONS |
ALPHA3_PERMISSIONS |
(VIEWER_PERMISSIONS | EDITOR_PERMISSIONS)
}

View file

@ -0,0 +1,15 @@
import { CheckCircledIcon, CrossCircledIcon, QuestionMarkCircledIcon } from '@radix-ui/react-icons'
export enum RecipeStatus {
DISABLE = 'DISABLE',
ENABLE = 'ENABLE'
}
const statusIcons = {
[RecipeStatus.DISABLE]: CrossCircledIcon,
[RecipeStatus.ENABLE]: CheckCircledIcon
}
export const getRecipeStatusIcon = (status: RecipeStatus) => {
return statusIcons[status] || QuestionMarkCircledIcon
}

View file

@ -0,0 +1,16 @@
import customAxios from '@/lib/customAxios'
import { type RecipeOverview } from '@/models/recipe/schema'
interface GetRecipeOverviewFilterQuery {
countryID: string
filename: string
materialIDs: string
}
export const getRecipeOverview = (query?: GetRecipeOverviewFilterQuery): Promise<RecipeOverview[]> => {
return customAxios
.get<RecipeOverview[]>(import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/recipe/overview', { params: query })
.then(res => {
return res.data
})
}

View file

@ -1,18 +1,11 @@
import { create } from 'zustand'
import customAxios from '../lib/customAxios'
export interface UserInfo {
id: string
name: string
email: string
picture: string
permissions: string[]
}
import type { User } from '@/models/user/schema'
interface UserAuth {
userInfo: UserInfo | null
setUserInfo: (userInfo: UserInfo | null) => void
getUserInfo: () => Promise<UserInfo | null>
userInfo: User | null
setUserInfo: (userInfo: User | null) => void
getUserInfo: () => Promise<User | null>
logout: () => void
}
@ -21,7 +14,7 @@ const userAuthStore = create<UserAuth>(set => ({
setUserInfo: userInfo => set({ userInfo }),
getUserInfo: () => {
return customAxios
.get<UserInfo>('/user/me')
.get<User>('/user/me')
.then(res => {
set({ userInfo: res.data })
return res.data

View file

@ -1,12 +1,16 @@
import { Outlet, useNavigate } from 'react-router-dom'
import Navbar from '../components/Header'
import Sidebar from '../components/Sidebar'
import Navbar from '../components/header'
import Sidebar, { type MenuList } from '../components/sidebar'
import userAuthStore from '../hooks/userAuth'
import { useShallow } from 'zustand/react/shallow'
import { useCallback } from 'react'
import { Toaster } from '@/components/ui/toaster'
const MainLayout = () => {
interface MainLayoutProps {
sidebarMenu: MenuList
}
const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
const { userInfo, getUserInfo } = userAuthStore(
useShallow(state => ({
userInfo: state.userInfo,
@ -33,7 +37,7 @@ const MainLayout = () => {
return (
<>
<Navbar />
<Sidebar />
<Sidebar menuList={sidebarMenu} />
<main className="p-8 sm:ml-64 mt-20">
<Outlet />
<Toaster />

View file

@ -1,7 +1,7 @@
import axios from 'axios'
const customAxios = axios.create({
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL,
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
withCredentials: true
})

View file

@ -0,0 +1,32 @@
import { PermissionEnum } from '@/constants/permissions'
export function getPermissions(permissions: number): PermissionEnum[] {
const permissionsArray: PermissionEnum[] = []
if (permissions & PermissionEnum.THAI_PERMISSIONS) {
permissionsArray.push(PermissionEnum.THAI_PERMISSIONS)
}
if (permissions & PermissionEnum.MALAY_PERMISSIONS) {
permissionsArray.push(PermissionEnum.MALAY_PERMISSIONS)
}
if (permissions & PermissionEnum.AUS_PERMISSIONS) {
permissionsArray.push(PermissionEnum.AUS_PERMISSIONS)
}
if (permissions & PermissionEnum.ALPHA3_PERMISSIONS) {
permissionsArray.push(PermissionEnum.ALPHA3_PERMISSIONS)
}
if (permissions & PermissionEnum.VIEWER_PERMISSIONS) {
permissionsArray.push(PermissionEnum.VIEWER_PERMISSIONS)
}
if (permissions & PermissionEnum.EDITOR_PERMISSIONS) {
permissionsArray.push(PermissionEnum.EDITOR_PERMISSIONS)
}
return permissionsArray
}
export function hasPermission(permissions: number, permission: PermissionEnum): boolean {
return Boolean(permissions & permission)
}
export function permissionsToNumber(permissions: PermissionEnum[]): number {
return permissions.reduce((acc, permission) => acc | permission, 0)
}

View file

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))

View file

@ -19,10 +19,7 @@ if (window.electronRuntime) {
})
window.ipcRenderer
.invoke(
'keyChainSync',
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME
)
.invoke('keyChainSync', import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME)
.then(result => {
console.log(result)
})

View file

@ -0,0 +1,20 @@
import { RecipeStatus } from '@/constants/recipe'
import { z } from 'zod'
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
})
export type Task = z.infer<typeof taskSchema>
export const recipeOverviewSchema = z.object({
productCode: z.string(),
name: z.string(),
otherName: z.string(),
status: z.nativeEnum(RecipeStatus),
lastUpdated: z.date()
})
export type RecipeOverview = z.infer<typeof recipeOverviewSchema>

View file

@ -0,0 +1,10 @@
import { z } from 'zod'
export const userSchema = z.object({
name: z.string(),
email: z.string().email(),
picture: z.string().url(),
permissions: z.number()
})
export type User = z.infer<typeof userSchema>

View file

@ -82,7 +82,9 @@ const AndroidPage: React.FC = () => {
)
async function scrcpyConnect() {
const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res => res.arrayBuffer())
const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
res.arrayBuffer()
)
await AdbScrcpyClient.pushServer(
adb!,
@ -94,7 +96,9 @@ const AndroidPage: React.FC = () => {
})
)
const res = await adb!.subprocess.spawn('CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25')
const res = await adb!.subprocess.spawn(
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
)
res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
new WritableStream({
@ -111,7 +115,12 @@ const AndroidPage: React.FC = () => {
control: true,
logLevel: ScrcpyLogLevel1_18.Debug
})
const _client = await AdbScrcpyClient.start(adb!, '/data/local/tmp/scrcpy-server.jar', '1.25', new AdbScrcpyOptions1_22(scrcpyOption))
const _client = await AdbScrcpyClient.start(
adb!,
'/data/local/tmp/scrcpy-server.jar',
'1.25',
new AdbScrcpyOptions1_22(scrcpyOption)
)
const videoStream: AdbScrcpyVideoStream | undefined = await _client?.videoStream
@ -241,7 +250,7 @@ const AndroidPage: React.FC = () => {
}
return (
<div className="flex justify-center items-center mx-auto">
<div className="flex flex-col justify-center items-center mx-auto">
<div className="flex justify-around items-start mx-auto w-full h-full">
<div>
<div ref={screenRef} className="min-h-[700px] min-w-[400px] max-h-[700px] max-w-[400px] bg-gray-700"></div>

View file

@ -1,13 +1,13 @@
import { useNavigate } from 'react-router-dom'
import googleLogo from '../assets/google-color.svg'
import userAuthStore, { type UserInfo } from '../hooks/userAuth'
import googleLogo from '@/assets/google-color.svg'
import userAuthStore from '@/hooks/userAuth'
import { userSchema } from '@/models/user/schema'
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') ?? '/'
const loginWithGoogle = () => {
// if is web mode then use window.open
@ -15,24 +15,35 @@ const LoginPage: React.FC = () => {
if (window.electronRuntime) {
window.ipcRenderer.send(
'deeplink',
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL +
'/auth/google?redirect_to=' +
redirectUrl +
'&kind=electron'
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl + '&kind=electron'
)
window.ipcRenderer.on('loginSuccess', (_event, data) => {
console.log(data)
setUserInfo(data satisfies UserInfo)
const { access_token, max_age, refresh_token } = data
window.ipcRenderer.send('set-keyChain', {
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
password: access_token + ';' + max_age
})
window.ipcRenderer.send('set-keyChain', {
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
password: refresh_token
})
data.permissions = Number(data.permissions)
const user = userSchema.parse(data)
setUserInfo(user)
navigate(redirectUrl)
})
} else {
// open new window and listen to message from window.opener
window.open(
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL +
'/auth/google?redirect_to=' +
redirectUrl,
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl,
'_blank',
'width=500,height=600'
)
@ -40,22 +51,8 @@ const LoginPage: React.FC = () => {
// listen to message from new window
window.addEventListener('message', event => {
if (event.data.payload === 'loginSuccess') {
// const { access_token, max_age, refresh_token } = event.data.data
// setPassword(
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
// import.meta.env
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
// access_token + ';' + max_age
// )
// setPassword(
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
// import.meta.env
// .TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
// refresh_token
// )
setUserInfo(event.data.data satisfies UserInfo)
const user = userSchema.parse(event.data.data)
setUserInfo(user)
navigate(redirectUrl)
}
@ -64,17 +61,12 @@ const LoginPage: React.FC = () => {
}
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex h-screen flex-col items-center justify-center">
<button
onClick={loginWithGoogle}
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
className="flex gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-slate-700 transition duration-150 hover:border-slate-400 hover:text-slate-900 hover:shadow"
>
<img
className="w-6 h-6"
src={googleLogo}
alt="google logo"
loading="eager"
/>
<img className="h-6 w-6" src={googleLogo} alt="google logo" loading="eager" />
<span>Login with @forth.co.th Google account</span>
</button>
</div>

View file

@ -1,12 +1,12 @@
import { type ColumnDef } from '@tanstack/react-table'
import { type Task } from '../models/schema'
import { type RecipeOverview } from '@/models/recipe/schema'
import { Checkbox } from '@/components/ui/checkbox'
import DataTableColumnHeader from './data-table-column-header'
import { labels, priorities, statuses } from '../models/data'
import { Badge } from '@/components/ui/badge'
import DataTableRowActions from './data-table-row-actions'
import { type RecipeStatus, getRecipeStatusIcon } from '@/constants/recipe'
export const columns: ColumnDef<Task>[] = [
export const columns: ColumnDef<RecipeOverview>[] = [
{
id: 'select',
header: ({ table }) => (
@ -29,22 +29,22 @@ export const columns: ColumnDef<Task>[] = [
enableHiding: false
},
{
accessorKey: 'id',
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
cell: ({ row }) => <div className="w-[80px]">{row.getValue('id')}</div>,
accessorKey: 'productCode',
header: ({ column }) => <DataTableColumnHeader column={column} title="ProductCode" />,
cell: ({ row }) => <div className="w-[80px]">{row.getValue('productCode')}</div>,
enableSorting: false,
enableHiding: false
},
{
accessorKey: 'title',
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
cell: ({ row }) => {
const label = labels.find(label => label.value === row.original.label)
const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
return (
<div className="flex space-x-2">
{label && <Badge variant="outline">{label.label}</Badge>}
<span className="max-w-[500px] truncate font-medium">{row.getValue('title')}</span>
<span className="max-w-[500px] truncate font-medium">{row.getValue('name')}</span>
</div>
)
}
@ -53,16 +53,16 @@ export const columns: ColumnDef<Task>[] = [
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const status = statuses.find(status => status.value === row.getValue('status'))
const status: RecipeStatus = row.getValue('status')
const StatusIcon = getRecipeStatusIcon(status)
if (!status) {
return null
}
return (
<div className="flex w-[100px] items-center">
{status.icon && <status.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
<span>{status.label}</span>
{<StatusIcon className="text-muted-foreground mr-2 h-4 w-4" />}
<span>{}</span>
</div>
)
},
@ -71,19 +71,12 @@ export const columns: ColumnDef<Task>[] = [
}
},
{
accessorKey: 'priority',
header: ({ column }) => <DataTableColumnHeader column={column} title="Priority" />,
accessorKey: 'lastUpdated',
header: ({ column }) => <DataTableColumnHeader column={column} title="Last Updated" />,
cell: ({ row }) => {
const priority = priorities.find(priority => priority.value === row.getValue('priority'))
if (!priority) {
return null
}
return (
<div className="flex items-center">
{priority.icon && <priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
<span>{priority.label}</span>
<span>{row.getValue('lastUpdated')}</span>
</div>
)
},

View file

@ -1,5 +1,5 @@
import { type Row } from '@tanstack/react-table'
import { taskSchema } from '../models/schema'
import { taskSchema } from '@/models/recipe/schema'
import {
DropdownMenu,
DropdownMenuContent,
@ -15,7 +15,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Button } from '@/components/ui/button'
import { DotsHorizontalIcon } from '@radix-ui/react-icons'
import { labels } from '../models/data'
import { labels } from '@/models/data'
interface DataTableRowActionsProps<TData> {
row: Row<TData>
@ -27,7 +27,7 @@ const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) =
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex h-8 w-8 p-0 data-[state=open]:bg-muted">
<Button variant="ghost" className="data-[state=open]:bg-muted flex h-8 w-8 p-0">
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>

View file

@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { type Table } from '@tanstack/react-table'
import { statuses, priorities } from '../models/data'
import { statuses, priorities } from '@/models/data'
import DataTableFacetedFilter from './data-table-faceted-filter'
import DataTableViewOptions from './data-table-view-options'
import { Cross2Icon } from '@radix-ui/react-icons'
@ -15,7 +15,7 @@ const DataTableToolbar = <TData,>({ table }: DataTableToolbarProps<TData>) => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col w-full rounded-lg bg-white shadow-md p-3 space-y-4">
<div className="flex w-full flex-col space-y-4 rounded-lg bg-white p-3 shadow-md">
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2">
<Input

View file

@ -1,11 +0,0 @@
import { z } from 'zod'
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
})
export type Task = z.infer<typeof taskSchema>

View file

@ -1,30 +1,21 @@
import { type Task, taskSchema } from './models/schema'
import { z } from 'zod'
import DataTable from './components/data-table'
import { columns } from './components/columns'
import { useQuery } from '@tanstack/react-query'
// Simulate a database read for tasks.
async function getTasks() {
const data = await fetch(new URL('./models/tasks.json', import.meta.url)).then(res => res.text())
const tasks = JSON.parse(data.toString())
return z.array(taskSchema).parse(tasks)
}
import { columns } from './components/columns'
import DataTable from './components/data-table'
import { getRecipeOverview } from '@/hooks/recipe/get-recipe-overview'
const RecipesPage = () => {
const { data: tasks, isLoading } = useQuery({ queryKey: ['tasks'], queryFn: getTasks })
console.log('here')
const { data: recipeOverviewList, isLoading } = useQuery({
queryKey: ['recipe-overview'],
queryFn: () => getRecipeOverview()
})
return (
<div className="flex flex-col w-full gap-3">
<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={tasks} columns={columns} isLoading={isLoading} />
<DataTable data={recipeOverviewList ?? []} columns={columns} isLoading={isLoading} />
</section>
</div>
)

View file

@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import Uppy from '@uppy/core'
import { Dashboard } from '@uppy/react'
import GoldenRetriever from '@uppy/golden-retriever'
import Tus from '@uppy/tus'
import '@uppy/core/dist/style.min.css'
import '@uppy/dashboard/dist/style.min.css'
import axios from 'axios'
const UploadPage: React.FC = () => {
const [uppy] = useState(() =>
new Uppy().use(GoldenRetriever, { serviceWorker: true }).use(Tus, {
endpoint: import.meta.env.TAOBIN_RECIPE_MANAGER_TUS_SERVER_URL ?? 'http://localhost:8090/files/',
withCredentials: true
})
)
const [files, setFiles] = useState([])
async function getFiles() {
const files = await axios.get('http://localhost:8090/list').then(res => res.data)
console.log(files)
setFiles(files)
}
useEffect(() => {
getFiles()
}, [])
uppy.on('upload-success', () => {
getFiles()
})
return (
<div className="flex justify-between items-center">
<Dashboard uppy={uppy} proudlyDisplayPoweredByUppy={false} showProgressDetails={true} />
<div className="flex flex-col">
{files.map((file, index) => (
<div key={index}>{JSON.stringify(file)}</div>
))}
</div>
</div>
)
}
export default UploadPage

View file

@ -2,6 +2,7 @@
interface ImportMetaEnv {
TAOBIN_RECIPE_MANAGER_SERVER_URL: string
TAOBIN_RECIPE_MANAGER_TUS_SERVER_URL: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN: string
TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN: string