update config future
This commit is contained in:
parent
cfc91b98a9
commit
1764f6fe96
|
|
@ -5,27 +5,29 @@ import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
|
|||
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||
tseslint.configs.recommended,
|
||||
eslintPluginReact.configs.flat.recommended,
|
||||
eslintPluginReact.configs.flat['jsx-runtime'],
|
||||
{
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': eslintPluginReactHooks,
|
||||
'react-refresh': eslintPluginReactRefresh
|
||||
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||
tseslint.configs.recommended,
|
||||
eslintPluginReact.configs.flat.recommended,
|
||||
eslintPluginReact.configs.flat['jsx-runtime'],
|
||||
{
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginReactHooks.configs.recommended.rules,
|
||||
...eslintPluginReactRefresh.configs.vite.rules
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': eslintPluginReactHooks,
|
||||
'react-refresh': eslintPluginReactRefresh
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginReactHooks.configs.recommended.rules,
|
||||
...eslintPluginReactRefresh.configs.vite.rules,
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@
|
|||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/form": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/notifications": "^8.0.0",
|
||||
"@mantine/vanilla-extract": "^8.0.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"axios": "^1.9.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"moment": "^2.30.1",
|
||||
|
|
@ -1803,6 +1805,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/form": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.0.0.tgz",
|
||||
"integrity": "sha512-ErbbEFMEiRsK2Rn0jmFE5ohNJXHSMSbuJsL2vDUVsbIaXo6svw6ockw1WWGdiU8oEGqxM6Pd618yI9cJWNHF3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"klona": "^2.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.x || ^19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@mantine/hooks": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.0.tgz",
|
||||
|
|
@ -2276,6 +2291,32 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.31.0.tgz",
|
||||
"integrity": "sha512-dblAdeKY3+GA1U+Q9eziZ0ooVlZMHsE8dqP0RkwvRtEsAULoKOYaCUOcJ4oW1DjWegdxk++UAt2SlQVnmeHv+g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons-react": {
|
||||
"version": "3.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.31.0.tgz",
|
||||
"integrity": "sha512-2rrCM5y/VnaVKnORpDdAua9SEGuJKVqPtWxeQ/vUVsgaUx30LDgBZph7/lterXxDY1IKR6NO//HDhWiifXTi3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tabler/icons": "3.31.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
|
|
@ -5660,7 +5701,6 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
|
|
@ -7370,6 +7410,15 @@
|
|||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/klona": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
|
||||
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy-val": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@
|
|||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@mantine/core": "^8.0.0",
|
||||
"@mantine/form": "^8.0.0",
|
||||
"@mantine/hooks": "^8.0.0",
|
||||
"@mantine/notifications": "^8.0.0",
|
||||
"@mantine/vanilla-extract": "^8.0.0",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"axios": "^1.9.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"moment": "^2.30.1",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { app, shell, BrowserWindow, ipcMain, screen, globalShortcut } from 'electron'
|
||||
import { join } from 'path'
|
||||
import path, { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import fs from 'fs'
|
||||
|
||||
function createWindow(): void {
|
||||
// Get Screen width, height
|
||||
|
|
@ -106,3 +109,56 @@ app.on('window-all-closed', () => {
|
|||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
// Custom events
|
||||
|
||||
ipcMain.handle('save-config-file', async (_event, jsonContent: any) => {
|
||||
// const filePath = path.join(__dirname, 'config-data.json')
|
||||
const userDataPath = app.getPath('userData')
|
||||
const filePath = path.join(userDataPath, 'config-data.json')
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const now = new Date().getTime()
|
||||
|
||||
// 📦 Ghi dữ liệu mới
|
||||
const newData = {
|
||||
...jsonContent, // giữ dữ liệu mới (nếu bạn muốn merge thì sửa dòng này)
|
||||
created_at: fileData.created_at || now, // nếu có created_at thì giữ nguyên, không có thì lấy now
|
||||
updated_at: now // luôn cập nhật updated_at
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(newData, null, 2), 'utf-8')
|
||||
|
||||
return { success: true, path: filePath }
|
||||
} catch (error: any) {
|
||||
console.error('Error saving file:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('get-config-file', async () => {
|
||||
const userDataPath = app.getPath('userData')
|
||||
const filePath = path.join(userDataPath, 'config-data.json')
|
||||
|
||||
try {
|
||||
// 📂 Kiểm tra file có tồn tại không
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { success: false, error: 'Config file does not exist' }
|
||||
}
|
||||
|
||||
const rawData = fs.readFileSync(filePath, 'utf-8')
|
||||
const fileData = JSON.parse(rawData)
|
||||
|
||||
return fileData
|
||||
} catch (error: any) {
|
||||
console.error('Error reading config file:', error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { contextBridge } from 'electron'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
// Custom APIs for renderer
|
||||
|
|
@ -8,15 +9,21 @@ const api = {}
|
|||
// 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)
|
||||
}
|
||||
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
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import '@mantine/core/styles.css'
|
||||
import '@mantine/notifications/styles.css'
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react'
|
||||
import { pusher } from './pusher'
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Container,
|
||||
List,
|
||||
|
|
@ -11,27 +9,53 @@ import {
|
|||
MantineProvider,
|
||||
Skeleton,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core'
|
||||
import { theme } from './theme'
|
||||
import CardItem from './components/CardItem'
|
||||
import { Product } from '@renderer/types/Product'
|
||||
import { listHotItem } from './api/products'
|
||||
import '@mantine/core/styles.css'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
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 { 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 moment from 'moment'
|
||||
|
||||
const PUSHER_CHANNEL = import.meta.env.VITE_PUSHER_CHANNEL
|
||||
const PUSHER_EVENT = 'App\\Events\\MessagePushed'
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
const [newItem, setNewItem] = useState<Array<Product>>([])
|
||||
const [newItems, setnewItems] = useState<Array<Product>>([])
|
||||
const [hotItem, setHotItem] = useState<any>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [timeout, setTimeout] = useState(2000)
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
|
||||
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 })
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(data.noti_timeout * 1000)
|
||||
} catch (error) {
|
||||
console.log('%csrc/renderer/src/App.tsx:48 error', 'color: #007acc;', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Subcribe Channel
|
||||
|
|
@ -40,31 +64,34 @@ function App(): React.JSX.Element {
|
|||
// Listen Event
|
||||
const callback = (data: any) => {
|
||||
if (data.data && data.data?.id) {
|
||||
setNewItem((prev: Array<Product>) => [data.data, ...prev])
|
||||
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>
|
||||
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: false,
|
||||
color: 'orange'
|
||||
})
|
||||
<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()
|
||||
|
|
@ -77,16 +104,20 @@ function App(): React.JSX.Element {
|
|||
channel.unbind(PUSHER_EVENT, callback)
|
||||
pusher.unsubscribe(PUSHER_CHANNEL)
|
||||
}
|
||||
}, [])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timeout, hotItem])
|
||||
|
||||
useEffect(() => {
|
||||
// load timeout config form local
|
||||
loadTimeOutData()
|
||||
|
||||
getListHotItem()
|
||||
}, [])
|
||||
|
||||
const getListHotItem = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
let data = await listHotItem()
|
||||
const data = await listHotItem()
|
||||
setHotItem(data)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
|
@ -95,6 +126,15 @@ function App(): React.JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
const isHotItem = useCallback(
|
||||
(item: Product) => {
|
||||
return hotItem.some((obj: any) =>
|
||||
item.title.toLowerCase().includes(obj.name.toLowerCase())
|
||||
)
|
||||
},
|
||||
[hotItem]
|
||||
)
|
||||
|
||||
return (
|
||||
<MantineProvider theme={theme}>
|
||||
<Notifications limit={10} store={myNotificationStore} position="top-right" />
|
||||
|
|
@ -109,6 +149,14 @@ function App(): React.JSX.Element {
|
|||
}
|
||||
>
|
||||
<Container fluid mt={8}>
|
||||
<Box mb={'xs'}>
|
||||
<Tooltip label="Settings">
|
||||
<ActionIcon onClick={open} variant="subtle" radius="md" size="lg">
|
||||
<IconDotsVertical size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Box>
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
|
|
@ -119,23 +167,19 @@ function App(): React.JSX.Element {
|
|||
</Box>
|
||||
) : (
|
||||
<List>
|
||||
{newItem?.length === 0 ? (
|
||||
{newItems?.length === 0 ? (
|
||||
<Box ta="center" mt={10}>
|
||||
<Title c="#495057" order={4}>
|
||||
No new item found!!!
|
||||
</Title>
|
||||
</Box>
|
||||
) : (
|
||||
newItem?.map((item: Product) => {
|
||||
newItems?.map((item: Product) => {
|
||||
return (
|
||||
<CardItem
|
||||
key={item?.id}
|
||||
item={item}
|
||||
hotItem={hotItem.some((obj: any) =>
|
||||
item.title
|
||||
.toLowerCase()
|
||||
.includes(obj.name.toLowerCase())
|
||||
)}
|
||||
hotItem={isHotItem(item)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
@ -143,6 +187,8 @@ function App(): React.JSX.Element {
|
|||
</List>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<ConfigModal onSaved={loadTimeOutData} opened={opened} onClose={close} />
|
||||
</Suspense>
|
||||
</MantineProvider>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import axios from './api-instance'
|
||||
|
||||
export const listHotItem = async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { Modal, ModalProps, NumberInput, Button, Stack } from '@mantine/core'
|
||||
import { useForm } from '@mantine/form'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export interface IConfigModalProps extends ModalProps {
|
||||
onSaved?: () => void
|
||||
}
|
||||
|
||||
export default function ConfigModal(props: IConfigModalProps) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
timeout: 30 // default timeout 30s
|
||||
},
|
||||
validate: {
|
||||
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null)
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.isValid()) return // Nếu form lỗi thì không save
|
||||
|
||||
const jsonContent = {
|
||||
noti_timeout: form.values.timeout
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electron.ipcRenderer.invoke('save-config-file', jsonContent)
|
||||
if (result.success) {
|
||||
props.onClose?.()
|
||||
|
||||
props.onSaved?.()
|
||||
} else {
|
||||
console.error('Failed to save file:', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('IPC error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadConfigData = async () => {
|
||||
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 })
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'%csrc/renderer/src/components/ConfigModal.tsx:41 error',
|
||||
'color: #007acc;',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (props.opened) {
|
||||
loadConfigData()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.opened])
|
||||
return (
|
||||
<Modal title="Configuration" {...props}>
|
||||
<form onSubmit={form.onSubmit(handleSave)}>
|
||||
<Stack>
|
||||
<NumberInput
|
||||
label="Timeout for notification (seconds)"
|
||||
placeholder="Enter timeout in seconds"
|
||||
min={1}
|
||||
{...form.getInputProps('timeout')}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth>
|
||||
Save
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { createRoot } from 'react-dom/client'
|
|||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Pusher from 'pusher-js'
|
||||
|
||||
export const pusher = new Pusher(import.meta.env.VITE_PUSHER_APP_KEY, {
|
||||
cluster: 'ap4',
|
||||
encrypted: true
|
||||
cluster: 'ap4'
|
||||
// encrypted: true
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,21 +1,54 @@
|
|||
// /* 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'
|
||||
|
||||
export const myNotificationStore = createNotificationsStore()
|
||||
|
||||
export function prependNotification(notification: NotificationData) {
|
||||
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)
|
||||
const shouldPrepend = callback ? callback() : false
|
||||
|
||||
return {
|
||||
...current,
|
||||
notifications: [
|
||||
{
|
||||
...notification,
|
||||
id
|
||||
},
|
||||
...existing
|
||||
]
|
||||
notifications: shouldPrepend
|
||||
? [
|
||||
{ ...notification, id }, // 👉 thêm lên đầu nếu callback = true
|
||||
...existing
|
||||
]
|
||||
: [
|
||||
...existing,
|
||||
{ ...notification, id } // 👉 thêm xuống cuối nếu callback = false
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue