test upload file to server
This commit is contained in:
parent
16e0e4f9d8
commit
aaa60216b2
43 changed files with 1814 additions and 285 deletions
|
|
@ -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 />
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
76
client-electron/src/components/ui/card.tsx
Normal file
76
client-electron/src/components/ui/card.tsx
Normal 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 }
|
||||
71
client-electron/src/components/ui/drop-zone.tsx
Normal file
71
client-electron/src/components/ui/drop-zone.tsx
Normal 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
|
||||
15
client-electron/src/constants/permissions.ts
Normal file
15
client-electron/src/constants/permissions.ts
Normal 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)
|
||||
}
|
||||
15
client-electron/src/constants/recipe.ts
Normal file
15
client-electron/src/constants/recipe.ts
Normal 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
|
||||
}
|
||||
16
client-electron/src/hooks/recipe/get-recipe-overview.ts
Normal file
16
client-electron/src/hooks/recipe/get-recipe-overview.ts
Normal 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
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
|||
32
client-electron/src/lib/permissions.ts
Normal file
32
client-electron/src/lib/permissions.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
20
client-electron/src/models/recipe/schema.ts
Normal file
20
client-electron/src/models/recipe/schema.ts
Normal 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>
|
||||
10
client-electron/src/models/user/schema.ts
Normal file
10
client-electron/src/models/user/schema.ts
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
46
client-electron/src/pages/upload.tsx
Normal file
46
client-electron/src/pages/upload.tsx
Normal 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
|
||||
1
client-electron/src/vite-env.d.ts
vendored
1
client-electron/src/vite-env.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue