Compare commits
	
		
			10 Commits
		
	
	
		
			zelda.add-
			...
			main
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						bdc9a49da2 | |
| 
							
							
								 | 
						6f9442a02e | |
| 
							
							
								 | 
						933bed2c81 | |
| 
							
							
								 | 
						72aa26c880 | |
| 
							
							
								 | 
						f8354882a7 | |
| 
							
							
								
								 | 
						d22267e3f8 | |
| 
							
							
								 | 
						ad585e276e | |
| 
							
							
								
								 | 
						85665b39c4 | |
| 
							
							
								 | 
						87eca555a2 | |
| 
							
							
								
								 | 
						a64ae2aa7a | 
| 
						 | 
				
			
			@ -1,43 +1,59 @@
 | 
			
		|||
appId: com.electron.app
 | 
			
		||||
productName: New Item
 | 
			
		||||
 | 
			
		||||
directories:
 | 
			
		||||
    buildResources: build
 | 
			
		||||
  buildResources: build
 | 
			
		||||
 | 
			
		||||
files:
 | 
			
		||||
    - '!**/.vscode/*'
 | 
			
		||||
    - '!src/*'
 | 
			
		||||
    - '!electron.vite.config.{js,ts,mjs,cjs}'
 | 
			
		||||
    - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
 | 
			
		||||
    - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
 | 
			
		||||
    - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
 | 
			
		||||
  - '!**/.vscode/*'
 | 
			
		||||
  - '!src/*'
 | 
			
		||||
  - '!electron.vite.config.{js,ts,mjs,cjs}'
 | 
			
		||||
  - '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
 | 
			
		||||
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
 | 
			
		||||
  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
 | 
			
		||||
 | 
			
		||||
asarUnpack:
 | 
			
		||||
    - resources/**
 | 
			
		||||
  - resources/**
 | 
			
		||||
 | 
			
		||||
win:
 | 
			
		||||
    executableName: New-item
 | 
			
		||||
nsis:
 | 
			
		||||
    artifactName: ${name}-${version}-setup.${ext}
 | 
			
		||||
    shortcutName: ${productName}
 | 
			
		||||
    uninstallDisplayName: ${productName}
 | 
			
		||||
    createDesktopShortcut: always
 | 
			
		||||
  target: nsis
 | 
			
		||||
  executableName: New-item
 | 
			
		||||
  publisherName: apactech
 | 
			
		||||
 | 
			
		||||
portable:
 | 
			
		||||
  artifactName: ${name}-${version}.exe
 | 
			
		||||
 | 
			
		||||
nsis:  # vẫn giữ để dùng nếu muốn cài đặt, nhưng sẽ không tạo nếu chỉ chọn portable
 | 
			
		||||
  artifactName: ${name}-${version}-setup.${ext}
 | 
			
		||||
  shortcutName: ${productName}
 | 
			
		||||
  uninstallDisplayName: ${productName}
 | 
			
		||||
  createDesktopShortcut: always
 | 
			
		||||
 | 
			
		||||
mac:
 | 
			
		||||
    entitlementsInherit: build/entitlements.mac.plist
 | 
			
		||||
    extendInfo:
 | 
			
		||||
        - NSCameraUsageDescription: Application requests access to the device's camera.
 | 
			
		||||
        - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
 | 
			
		||||
        - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
 | 
			
		||||
        - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
 | 
			
		||||
    notarize: false
 | 
			
		||||
  entitlementsInherit: build/entitlements.mac.plist
 | 
			
		||||
  extendInfo:
 | 
			
		||||
    - NSCameraUsageDescription: Application requests access to the device's camera.
 | 
			
		||||
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
 | 
			
		||||
    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
 | 
			
		||||
    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
 | 
			
		||||
  notarize: false
 | 
			
		||||
 | 
			
		||||
dmg:
 | 
			
		||||
    artifactName: ${name}-${version}.${ext}
 | 
			
		||||
  artifactName: ${name}-${version}.${ext}
 | 
			
		||||
 | 
			
		||||
linux:
 | 
			
		||||
    target:
 | 
			
		||||
        - AppImage
 | 
			
		||||
        - snap
 | 
			
		||||
        - deb
 | 
			
		||||
    maintainer: electronjs.org
 | 
			
		||||
    category: Utility
 | 
			
		||||
  target:
 | 
			
		||||
    - AppImage
 | 
			
		||||
    - snap
 | 
			
		||||
    - deb
 | 
			
		||||
  maintainer: electronjs.org
 | 
			
		||||
  category: Utility
 | 
			
		||||
 | 
			
		||||
appImage:
 | 
			
		||||
    artifactName: ${name}-${version}.${ext}
 | 
			
		||||
  artifactName: ${name}-${version}.${ext}
 | 
			
		||||
 | 
			
		||||
npmRebuild: false
 | 
			
		||||
 | 
			
		||||
publish:
 | 
			
		||||
    provider: generic
 | 
			
		||||
    url: https://example.com/auto-updates
 | 
			
		||||
  provider: generic
 | 
			
		||||
  url: https://apactech.io/auto-updates
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
{
 | 
			
		||||
    "name": "new-item-app",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "version": "1.0.5",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "new-item-app",
 | 
			
		||||
            "version": "1.0.0",
 | 
			
		||||
            "version": "1.0.5",
 | 
			
		||||
            "hasInstallScript": true,
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@electron-toolkit/preload": "^3.0.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,8 @@
 | 
			
		|||
                "axios": "^1.9.0",
 | 
			
		||||
                "electron-updater": "^6.3.9",
 | 
			
		||||
                "moment": "^2.30.1",
 | 
			
		||||
                "pusher-js": "^8.4.0"
 | 
			
		||||
                "pusher-js": "^8.4.0",
 | 
			
		||||
                "windows-shortcuts": "^0.1.6"
 | 
			
		||||
            },
 | 
			
		||||
            "devDependencies": {
 | 
			
		||||
                "@electron-toolkit/eslint-config-prettier": "^3.0.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -10979,6 +10980,12 @@
 | 
			
		|||
                "string-width": "^1.0.2 || 2 || 3 || 4"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/windows-shortcuts": {
 | 
			
		||||
            "version": "0.1.6",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/windows-shortcuts/-/windows-shortcuts-0.1.6.tgz",
 | 
			
		||||
            "integrity": "sha512-kjkb3Hmmmg7jwnOb+29AOmoEEA1L/JeLsMOYovpLxYpuc+fN0R+pr8sMwep3JFhUZloxyw1XTzq8n3HugXkqBA==",
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/word-wrap": {
 | 
			
		||||
            "version": "1.2.5",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
{
 | 
			
		||||
    "name": "new-item-app",
 | 
			
		||||
    "version": "1.0.0",
 | 
			
		||||
    "description": "An Electron application with React and TypeScript",
 | 
			
		||||
    "version": "1.0.5",
 | 
			
		||||
    "description": "new item application is active",
 | 
			
		||||
    "main": "./out/main/index.js",
 | 
			
		||||
    "author": "example.com",
 | 
			
		||||
    "author": "apactech.io",
 | 
			
		||||
    "homepage": "https://electron-vite.org",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "format": "prettier --write .",
 | 
			
		||||
| 
						 | 
				
			
			@ -32,7 +32,8 @@
 | 
			
		|||
        "axios": "^1.9.0",
 | 
			
		||||
        "electron-updater": "^6.3.9",
 | 
			
		||||
        "moment": "^2.30.1",
 | 
			
		||||
        "pusher-js": "^8.4.0"
 | 
			
		||||
        "pusher-js": "^8.4.0",
 | 
			
		||||
        "windows-shortcuts": "^0.1.6"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@electron-toolkit/eslint-config-prettier": "^3.0.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,11 @@ import { app, BrowserWindow, globalShortcut, ipcMain, Menu, screen, shell, Tray
 | 
			
		|||
import fs from 'fs'
 | 
			
		||||
import path, { join } from 'path'
 | 
			
		||||
import icon from '../../resources/icon.png?asset'
 | 
			
		||||
import ws from 'windows-shortcuts'
 | 
			
		||||
 | 
			
		||||
let mainWindow: null | BrowserWindow = null
 | 
			
		||||
let isQuiting = false
 | 
			
		||||
 | 
			
		||||
function createWindow(): void {
 | 
			
		||||
    // Get Screen width, height
 | 
			
		||||
    const { width, height } = screen.getPrimaryDisplay().workAreaSize
 | 
			
		||||
| 
						 | 
				
			
			@ -39,10 +41,7 @@ function createWindow(): void {
 | 
			
		|||
    // Make the window always on top
 | 
			
		||||
    mainWindow.setAlwaysOnTop(true, 'normal')
 | 
			
		||||
 | 
			
		||||
    // Right-click to Inspect Element
 | 
			
		||||
    // mainWindow.webContents.on('context-menu', (_, params) => {
 | 
			
		||||
    //     mainWindow?.webContents.inspectElement(params.x, params.y)
 | 
			
		||||
    // })
 | 
			
		||||
    // mainWindow.webContents.openDevTools()
 | 
			
		||||
 | 
			
		||||
    // Inspect element with shortcut
 | 
			
		||||
    mainWindow.webContents.once('did-finish-load', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +89,7 @@ function createTray() {
 | 
			
		|||
            label: 'Show',
 | 
			
		||||
            click: () => {
 | 
			
		||||
                mainWindow?.show()
 | 
			
		||||
                mainWindow?.focus()
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +113,22 @@ function createTray() {
 | 
			
		|||
// initialization and is ready to create browser windows.
 | 
			
		||||
// Some APIs can only be used after this event occurs.
 | 
			
		||||
app.whenReady().then(() => {
 | 
			
		||||
    // const startupFlagFile = path.join(app.getPath('userData'), 'startup-set.flag')
 | 
			
		||||
    const startupFolder = path.join(
 | 
			
		||||
        app.getPath('appData'),
 | 
			
		||||
        'Microsoft\\Windows\\Start Menu\\Programs\\Startup'
 | 
			
		||||
    )
 | 
			
		||||
    const shortcutPath = path.join(startupFolder, 'New Item.lnk')
 | 
			
		||||
 | 
			
		||||
    if (!fs.existsSync(shortcutPath)) {
 | 
			
		||||
        ws.create(shortcutPath, {
 | 
			
		||||
            target: process.execPath,
 | 
			
		||||
            workingDir: path.dirname(process.execPath),
 | 
			
		||||
            runStyle: 1,
 | 
			
		||||
            desc: 'Start New Item with Windows',
 | 
			
		||||
            icon: process.execPath
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    // Set app user model id for windows
 | 
			
		||||
    electronApp.setAppUserModelId('com.electron')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -132,6 +148,16 @@ app.whenReady().then(() => {
 | 
			
		|||
    // Create tray icon
 | 
			
		||||
    createTray()
 | 
			
		||||
 | 
			
		||||
    // Lắng nghe khi hiện cửa sổ
 | 
			
		||||
    mainWindow?.on('show', () => {
 | 
			
		||||
        mainWindow?.webContents.send('window-event', 'show')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // Lắng nghe khi ẩn cửa sổ
 | 
			
		||||
    mainWindow?.on('hide', () => {
 | 
			
		||||
        mainWindow?.webContents.send('window-event', 'hide')
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    app.on('activate', function () {
 | 
			
		||||
        // On macOS it's common to re-create a window in the app when the
 | 
			
		||||
        // dock icon is clicked and there are no other windows open.
 | 
			
		||||
| 
						 | 
				
			
			@ -160,7 +186,6 @@ ipcMain.handle('save-config-file', async (_event, jsonContent: any) => {
 | 
			
		|||
    try {
 | 
			
		||||
        let fileData: any = {}
 | 
			
		||||
 | 
			
		||||
        // 👀 Kiểm tra nếu file đã tồn tại
 | 
			
		||||
        if (fs.existsSync(filePath)) {
 | 
			
		||||
            const rawData = fs.readFileSync(filePath, 'utf-8')
 | 
			
		||||
            fileData = JSON.parse(rawData)
 | 
			
		||||
| 
						 | 
				
			
			@ -203,3 +228,12 @@ ipcMain.handle('get-config-file', async () => {
 | 
			
		|||
        return null
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
ipcMain.handle('show-window', async () => {
 | 
			
		||||
    mainWindow?.show()
 | 
			
		||||
    mainWindow?.focus()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
ipcMain.handle('hide-window', async () => {
 | 
			
		||||
    mainWindow?.hide()
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,29 +1,31 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
import { contextBridge, ipcRenderer } from 'electron'
 | 
			
		||||
import { contextBridge, ipcRenderer, shell } from 'electron'
 | 
			
		||||
import { electronAPI } from '@electron-toolkit/preload'
 | 
			
		||||
 | 
			
		||||
// Custom APIs for renderer
 | 
			
		||||
const api = {}
 | 
			
		||||
 | 
			
		||||
// Use `contextBridge` APIs to expose Electron APIs to
 | 
			
		||||
// renderer only if context isolation is enabled, otherwise
 | 
			
		||||
// just add to the DOM global.
 | 
			
		||||
if (process.contextIsolated) {
 | 
			
		||||
    try {
 | 
			
		||||
        contextBridge.exposeInMainWorld('electron', electronAPI)
 | 
			
		||||
        contextBridge.exposeInMainWorld('api', api)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error(error)
 | 
			
		||||
    }
 | 
			
		||||
} else {
 | 
			
		||||
    // @ts-ignore (define in dts)
 | 
			
		||||
    window.electron = electronAPI
 | 
			
		||||
    // @ts-ignore (define in dts)
 | 
			
		||||
    window.api = api
 | 
			
		||||
const customAPI = {
 | 
			
		||||
    ipcRenderer: {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
        invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
 | 
			
		||||
        on: (channel: string, listener: (...args: any[]) => void) =>
 | 
			
		||||
            ipcRenderer.on(channel, (_, ...args) => listener(...args)),
 | 
			
		||||
        removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel)
 | 
			
		||||
    },
 | 
			
		||||
    openExternal: (url: string) => shell.openExternal(url)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
contextBridge.exposeInMainWorld('electron', {
 | 
			
		||||
    ipcRenderer: {
 | 
			
		||||
        invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
 | 
			
		||||
// Gộp cả electronAPI và customAPI
 | 
			
		||||
const mergedAPI = {
 | 
			
		||||
    ...electronAPI,
 | 
			
		||||
    ...customAPI
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (process.contextIsolated) {
 | 
			
		||||
    try {
 | 
			
		||||
        contextBridge.exposeInMainWorld('electron', mergedAPI)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('Failed to expose electron API:', error)
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
} else {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    window.electron = mergedAPI
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,6 @@ import {
 | 
			
		|||
    LoadingOverlay,
 | 
			
		||||
    MantineProvider,
 | 
			
		||||
    Skeleton,
 | 
			
		||||
    Text,
 | 
			
		||||
    Title,
 | 
			
		||||
    Tooltip
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
| 
						 | 
				
			
			@ -18,18 +17,13 @@ import { Notifications } from '@mantine/notifications'
 | 
			
		|||
import '@mantine/notifications/styles.css'
 | 
			
		||||
import { Product } from '@renderer/types/Product'
 | 
			
		||||
import { IconDotsVertical } from '@tabler/icons-react'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import { Suspense, useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
 | 
			
		||||
import { listHotItem } from './api/products'
 | 
			
		||||
import CardItem from './components/CardItem'
 | 
			
		||||
import ConfigModal from './components/ConfigModal'
 | 
			
		||||
import { pusher } from './pusher'
 | 
			
		||||
import { theme } from './theme'
 | 
			
		||||
import {
 | 
			
		||||
    myNotificationStore,
 | 
			
		||||
    playNotificationSound,
 | 
			
		||||
    prependNotification
 | 
			
		||||
} from './utils/Notificaton'
 | 
			
		||||
import { myNotificationStore, playNotificationSound } from './utils/Notificaton'
 | 
			
		||||
 | 
			
		||||
const PUSHER_CHANNEL = import.meta.env.VITE_PUSHER_CHANNEL
 | 
			
		||||
const PUSHER_EVENT = 'App\\Events\\MessagePushed'
 | 
			
		||||
| 
						 | 
				
			
			@ -38,20 +32,36 @@ function App(): React.JSX.Element {
 | 
			
		|||
    const [newItems, setnewItems] = useState<Array<Product>>([])
 | 
			
		||||
    const [hotItem, setHotItem] = useState<any>([])
 | 
			
		||||
    const [isLoading, setIsLoading] = useState<boolean>(false)
 | 
			
		||||
    const [timeout, setTimeout] = useState(2000)
 | 
			
		||||
    const [jsonData, setJsonData] = useState({
 | 
			
		||||
        noti_timeout: 2000,
 | 
			
		||||
        delay_hide: 10000
 | 
			
		||||
    })
 | 
			
		||||
    const [mouseEnter, setmouseEnter] = useState(false)
 | 
			
		||||
 | 
			
		||||
    const [opened, { open, close }] = useDisclosure(false)
 | 
			
		||||
 | 
			
		||||
    const refMouseEnterTimeout = useRef<NodeJS.Timeout | null>(null)
 | 
			
		||||
    const refNotiTimeout = useRef<NodeJS.Timeout | null>(null)
 | 
			
		||||
    const appStartedRef = useRef(false)
 | 
			
		||||
 | 
			
		||||
    const jsonDataRef = useRef(jsonData)
 | 
			
		||||
 | 
			
		||||
    const loadTimeOutData = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            const data = await window.electron.ipcRenderer.invoke('get-config-file')
 | 
			
		||||
 | 
			
		||||
            if (!data?.noti_timeout) {
 | 
			
		||||
                await window.electron.ipcRenderer.invoke('save-config-file', { noti_timeout: 2 })
 | 
			
		||||
                await window.electron.ipcRenderer.invoke('save-config-file', {
 | 
			
		||||
                    noti_timeout: 2,
 | 
			
		||||
                    delay_hide: 10
 | 
			
		||||
                })
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setTimeout(data.noti_timeout * 1000)
 | 
			
		||||
            setJsonData({
 | 
			
		||||
                noti_timeout: data.noti_timeout * 1000,
 | 
			
		||||
                delay_hide: data.delay_hide * 1000
 | 
			
		||||
            })
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log('%csrc/renderer/src/App.tsx:48 error', 'color: #007acc;', error)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -66,35 +76,18 @@ function App(): React.JSX.Element {
 | 
			
		|||
            if (data.data && data.data?.id) {
 | 
			
		||||
                setnewItems((prev: Array<Product>) => [data.data, ...prev])
 | 
			
		||||
 | 
			
		||||
                // Show notification
 | 
			
		||||
                prependNotification(
 | 
			
		||||
                    {
 | 
			
		||||
                        title: (
 | 
			
		||||
                            <Title order={4} className="text-overflow-1-line">
 | 
			
		||||
                                {data.data?.title}
 | 
			
		||||
                            </Title>
 | 
			
		||||
                        ),
 | 
			
		||||
                        message: (
 | 
			
		||||
                            <Box>
 | 
			
		||||
                                <Text style={{ fontSize: 14 }} mb={2}>
 | 
			
		||||
                                    Price: <span className="bold-text">${data.data?.price}</span>,
 | 
			
		||||
                                    Site: <span className="bold-text">{data.data?.from_site}</span>,
 | 
			
		||||
                                    Seller: <span className="bold-text">{data.data?.seller}</span>
 | 
			
		||||
                                </Text>
 | 
			
		||||
 | 
			
		||||
                                <Text style={{ fontSize: 12 }}>
 | 
			
		||||
                                    {moment(new Date()).format('HH:mm A')}
 | 
			
		||||
                                </Text>
 | 
			
		||||
                            </Box>
 | 
			
		||||
                        ),
 | 
			
		||||
                        autoClose: isHotItem(data.data) ? false : timeout,
 | 
			
		||||
                        color: 'orange'
 | 
			
		||||
                    },
 | 
			
		||||
                    () => isHotItem(data.data)
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                // Play sound effect
 | 
			
		||||
                playNotificationSound()
 | 
			
		||||
 | 
			
		||||
                window.electron.ipcRenderer.invoke('show-window')
 | 
			
		||||
 | 
			
		||||
                clearTimeouts()
 | 
			
		||||
 | 
			
		||||
                if (isHotItem(data.data) || mouseEnter) return
 | 
			
		||||
 | 
			
		||||
                refNotiTimeout.current = setTimeout(() => {
 | 
			
		||||
                    window.electron.ipcRenderer.invoke('hide-window')
 | 
			
		||||
                }, jsonData.noti_timeout)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -105,7 +98,7 @@ function App(): React.JSX.Element {
 | 
			
		|||
            pusher.unsubscribe(PUSHER_CHANNEL)
 | 
			
		||||
        }
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, [timeout, hotItem])
 | 
			
		||||
    }, [jsonData.noti_timeout, hotItem, mouseEnter])
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // load timeout config form local
 | 
			
		||||
| 
						 | 
				
			
			@ -135,6 +128,77 @@ function App(): React.JSX.Element {
 | 
			
		|||
        [hotItem]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const handleMouseEnter = useCallback(() => {
 | 
			
		||||
        setmouseEnter(true)
 | 
			
		||||
 | 
			
		||||
        clearTimeouts()
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    // Đây là hàm không phụ thuộc jsonData nữa, lấy delay_hide từ ref
 | 
			
		||||
    const handleMouseLeave = useCallback(() => {
 | 
			
		||||
        setmouseEnter(false)
 | 
			
		||||
        if (refMouseEnterTimeout.current) {
 | 
			
		||||
            clearTimeout(refMouseEnterTimeout.current)
 | 
			
		||||
        }
 | 
			
		||||
        refMouseEnterTimeout.current = setTimeout(() => {
 | 
			
		||||
            window.electron.ipcRenderer.invoke('hide-window')
 | 
			
		||||
        }, jsonDataRef.current.delay_hide)
 | 
			
		||||
    }, [])
 | 
			
		||||
 | 
			
		||||
    const initApp = useCallback(() => {
 | 
			
		||||
        if (appStartedRef.current) return
 | 
			
		||||
        appStartedRef.current = true
 | 
			
		||||
 | 
			
		||||
        window.focus()
 | 
			
		||||
 | 
			
		||||
        document.addEventListener('mouseenter', handleMouseEnter)
 | 
			
		||||
        document.addEventListener('mouseleave', handleMouseLeave)
 | 
			
		||||
    }, [handleMouseEnter, handleMouseLeave])
 | 
			
		||||
 | 
			
		||||
    // Phần event lắng nghe window-event thì giữ nguyên
 | 
			
		||||
 | 
			
		||||
    // Phần clean up khi window ẩn:
 | 
			
		||||
    const cleanUpApp = useCallback(() => {
 | 
			
		||||
        document.removeEventListener('mouseenter', handleMouseEnter)
 | 
			
		||||
        document.removeEventListener('mouseleave', handleMouseLeave)
 | 
			
		||||
        clearTimeouts()
 | 
			
		||||
    }, [handleMouseEnter, handleMouseLeave])
 | 
			
		||||
 | 
			
		||||
    const clearTimeouts = () => {
 | 
			
		||||
        const timeoutIds = [refMouseEnterTimeout, refNotiTimeout]
 | 
			
		||||
 | 
			
		||||
        timeoutIds.forEach((timeout) => {
 | 
			
		||||
            if (!timeout.current) return
 | 
			
		||||
 | 
			
		||||
            clearTimeout(timeout.current)
 | 
			
		||||
            timeout.current = null
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const handler = (type: any) => {
 | 
			
		||||
            console.log('Window event:', type)
 | 
			
		||||
            if (type === 'show') {
 | 
			
		||||
                initApp()
 | 
			
		||||
            }
 | 
			
		||||
            if (type === 'hide') {
 | 
			
		||||
                appStartedRef.current = false
 | 
			
		||||
                cleanUpApp()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        window.electron.ipcRenderer.on('window-event', handler)
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            window.electron.ipcRenderer.removeAllListeners('window-event')
 | 
			
		||||
        }
 | 
			
		||||
    }, [initApp, cleanUpApp])
 | 
			
		||||
 | 
			
		||||
    // Cập nhật ref mỗi khi jsonData thay đổi
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        jsonDataRef.current = jsonData
 | 
			
		||||
    }, [jsonData])
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <MantineProvider theme={theme}>
 | 
			
		||||
            <Notifications limit={10} store={myNotificationStore} position="top-right" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,18 @@
 | 
			
		|||
@keyframes borderPulse {
 | 
			
		||||
    0% {
 | 
			
		||||
        border-color: red;
 | 
			
		||||
        border-color: #e03131;
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
        border-color: black;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        border-color: red;
 | 
			
		||||
        border-color: #e03131;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item-card {
 | 
			
		||||
    animation: borderPulse 1s infinite;
 | 
			
		||||
    border: 4px solid red;
 | 
			
		||||
    border: 4px solid #e03131;
 | 
			
		||||
    border-radius: 4px;
 | 
			
		||||
    transition: border-color 0.3s ease;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +34,11 @@
 | 
			
		|||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-overflow-1-line:hover {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.item-picture {
 | 
			
		||||
    height: auto;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
import { Card, Grid, Title, Text, Badge, Button, Anchor } from '@mantine/core'
 | 
			
		||||
import { Anchor, Box, Button, Card, Grid, Text, Title } from '@mantine/core'
 | 
			
		||||
import { Product } from '@renderer/types/Product'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import TimeCounter from './TimeCounter'
 | 
			
		||||
import { linkToItem } from '@renderer/utils/fn'
 | 
			
		||||
 | 
			
		||||
interface CardItemProps {
 | 
			
		||||
    item: Product
 | 
			
		||||
| 
						 | 
				
			
			@ -9,21 +10,62 @@ interface CardItemProps {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
const CardItem: React.FC<CardItemProps> = ({ item, hotItem }) => {
 | 
			
		||||
    const handleClickExternalLink = () => {
 | 
			
		||||
        window.electron?.openExternal(linkToItem(item))
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <Card className="item-card" mb={4} p={8}>
 | 
			
		||||
        <Card className="item-card" bg={'#f1f3f5'} mb={8} px={8} py={'xl'}>
 | 
			
		||||
            <Grid>
 | 
			
		||||
                <Grid.Col span={2}>
 | 
			
		||||
                <Grid.Col
 | 
			
		||||
                    span={2}
 | 
			
		||||
                    style={{
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        alignItems: 'center',
 | 
			
		||||
                        justifyContent: 'center'
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    {hotItem ? (
 | 
			
		||||
                        <Badge size="md" color="red" pos="absolute" radius={0}>
 | 
			
		||||
                            Hot
 | 
			
		||||
                        </Badge>
 | 
			
		||||
                        <Box
 | 
			
		||||
                            style={{
 | 
			
		||||
                                position: 'absolute',
 | 
			
		||||
                                top: 0,
 | 
			
		||||
                                right: 0,
 | 
			
		||||
                                width: 80,
 | 
			
		||||
                                height: 80,
 | 
			
		||||
                                backgroundColor: '#e03131',
 | 
			
		||||
                                clipPath: 'polygon(100% 0, 100% 100%, 0 0)',
 | 
			
		||||
                                display: 'flex',
 | 
			
		||||
                                justifyContent: 'end',
 | 
			
		||||
                                alignItems: 'flex-start',
 | 
			
		||||
                                paddingTop: 10
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <Text
 | 
			
		||||
                                c="white"
 | 
			
		||||
                                fw={700}
 | 
			
		||||
                                size="xs"
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    transform: 'rotate(45deg)',
 | 
			
		||||
                                    paddingTop: '10px',
 | 
			
		||||
                                    paddingRight: '8px'
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                HOT
 | 
			
		||||
                            </Text>
 | 
			
		||||
                        </Box>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        ''
 | 
			
		||||
                    )}
 | 
			
		||||
                    <img src={item?.picture} alt={item?.title} className="item-picture" />
 | 
			
		||||
                </Grid.Col>
 | 
			
		||||
                <Grid.Col span={10}>
 | 
			
		||||
                    <Title order={4} c="#0202bf" mb={4} className="text-overflow-1-line">
 | 
			
		||||
                    <Title
 | 
			
		||||
                        onClick={handleClickExternalLink}
 | 
			
		||||
                        order={4}
 | 
			
		||||
                        c="#0202bf"
 | 
			
		||||
                        mb={4}
 | 
			
		||||
                        className="text-overflow-1-line"
 | 
			
		||||
                    >
 | 
			
		||||
                        {item?.title}
 | 
			
		||||
                    </Title>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,12 @@ export interface IConfigModalProps extends ModalProps {
 | 
			
		|||
export default function ConfigModal(props: IConfigModalProps) {
 | 
			
		||||
    const form = useForm({
 | 
			
		||||
        initialValues: {
 | 
			
		||||
            timeout: 30 // default timeout 30s
 | 
			
		||||
            timeout: 10, // default timeout 30s
 | 
			
		||||
            delay_hide: 10
 | 
			
		||||
        },
 | 
			
		||||
        validate: {
 | 
			
		||||
            timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null)
 | 
			
		||||
            timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null),
 | 
			
		||||
            delay_hide: (value) => (value <= 0 ? 'Delay must be greater than 0' : null)
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +22,8 @@ export default function ConfigModal(props: IConfigModalProps) {
 | 
			
		|||
        if (!form.isValid()) return // Nếu form lỗi thì không save
 | 
			
		||||
 | 
			
		||||
        const jsonContent = {
 | 
			
		||||
            noti_timeout: form.values.timeout
 | 
			
		||||
            noti_timeout: form.values.timeout,
 | 
			
		||||
            delay_hide: form.values.delay_hide
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
| 
						 | 
				
			
			@ -41,14 +44,9 @@ export default function ConfigModal(props: IConfigModalProps) {
 | 
			
		|||
        try {
 | 
			
		||||
            const data = await window.electron.ipcRenderer.invoke('get-config-file')
 | 
			
		||||
 | 
			
		||||
            console.log(
 | 
			
		||||
                '%csrc/renderer/src/components/ConfigModal.tsx:44 data',
 | 
			
		||||
                'color: #007acc;',
 | 
			
		||||
                data
 | 
			
		||||
            )
 | 
			
		||||
            if (!data) return
 | 
			
		||||
 | 
			
		||||
            form.setValues({ timeout: data?.noti_timeout })
 | 
			
		||||
            form.setValues({ timeout: data?.noti_timeout, delay_hide: data.delay_hide })
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                '%csrc/renderer/src/components/ConfigModal.tsx:41 error',
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +72,12 @@ export default function ConfigModal(props: IConfigModalProps) {
 | 
			
		|||
                        min={1}
 | 
			
		||||
                        {...form.getInputProps('timeout')}
 | 
			
		||||
                    />
 | 
			
		||||
                    <NumberInput
 | 
			
		||||
                        label="Delay (seconds)"
 | 
			
		||||
                        placeholder="Enter timeout in seconds"
 | 
			
		||||
                        min={1}
 | 
			
		||||
                        {...form.getInputProps('delay_hide')}
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                    <Button type="submit" fullWidth>
 | 
			
		||||
                        Save
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,30 +1,3 @@
 | 
			
		|||
// /* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
			
		||||
// import { createNotificationsStore, NotificationData } from '@mantine/notifications'
 | 
			
		||||
// import notifySound from '../assets/notifty.mp3'
 | 
			
		||||
 | 
			
		||||
// export const myNotificationStore = createNotificationsStore()
 | 
			
		||||
 | 
			
		||||
// export function prependNotification(notification: NotificationData, callback?: () => boolean) {
 | 
			
		||||
//     const id = notification.id ?? crypto.randomUUID()
 | 
			
		||||
//     myNotificationStore.updateState((current) => {
 | 
			
		||||
//         const existing = current.notifications.filter((n) => n.id !== id)
 | 
			
		||||
//         return {
 | 
			
		||||
//             ...current,
 | 
			
		||||
//             notifications: [
 | 
			
		||||
//                 {
 | 
			
		||||
//                     ...notification,
 | 
			
		||||
//                     id
 | 
			
		||||
//                 },
 | 
			
		||||
//                 ...existing
 | 
			
		||||
//             ]
 | 
			
		||||
//         }
 | 
			
		||||
//     })
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// export const playNotificationSound = () => {
 | 
			
		||||
//     const audio = new Audio(notifySound)
 | 
			
		||||
//     audio.play().catch((e) => console.error('Audio play failed:', e))
 | 
			
		||||
// }
 | 
			
		||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
			
		||||
import { createNotificationsStore, NotificationData } from '@mantine/notifications'
 | 
			
		||||
import notifySound from '../assets/notifty.mp3'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
			
		||||
export const linkToItem = (item: any) => {
 | 
			
		||||
    switch (item.from_site) {
 | 
			
		||||
        case 'EBAY_AU':
 | 
			
		||||
            return `https://www.ebay.com.au/itm/${item.id}`
 | 
			
		||||
        case 'EBAY_ENCA':
 | 
			
		||||
            return `https://www.ebay.ca/itm/${item.id}`
 | 
			
		||||
        case 'EBAY_GB':
 | 
			
		||||
            return `https://www.ebay.co.uk/itm/${item.id}`
 | 
			
		||||
        default:
 | 
			
		||||
            return `https://www.ebay.com/itm/${item.id}`
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue