update save config

This commit is contained in:
Admin 2025-09-18 07:59:42 +07:00
parent 72f62d4cc5
commit ed00ebd174
19 changed files with 837 additions and 73 deletions

18
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "shotcut-app",
"version": "1.0.5",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "shotcut-app",
"version": "1.0.5",
"version": "1.0.1",
"hasInstallScript": true,
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
@ -21,6 +21,7 @@
"electron-updater": "^6.3.9",
"moment": "^2.30.1",
"pusher-js": "^8.4.0",
"uuid": "^13.0.0",
"windows-shortcuts": "^0.1.6"
},
"devDependencies": {
@ -10726,6 +10727,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@ -33,6 +33,7 @@
"electron-updater": "^6.3.9",
"moment": "^2.30.1",
"pusher-js": "^8.4.0",
"uuid": "^13.0.0",
"windows-shortcuts": "^0.1.6"
},
"devDependencies": {

View File

@ -1,20 +1,114 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { app, ipcMain, Menu, Tray } from 'electron'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, Tray, screen, shell } from 'electron'
import fs from 'fs'
import path from 'path'
import path, { join } from 'path'
import ws from 'windows-shortcuts'
import icon from '../../resources/icon.png?asset'
import { registerGlobalShortcuts, unregisterGlobalShortcuts } from './shortcut'
import { registerGlobalShortcuts, unregisterGlobalShortcuts } from './shotcut'
let mainWindow: null | BrowserWindow = null
// eslint-disable-next-line prefer-const
let isQuiting = false
function createWindow(): void {
// Get Screen width, height
const width = 800
const height = 600
mainWindow = new BrowserWindow({
width,
height,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
// Set App Show Position
mainWindow.setPosition(width / 2, height / 2)
mainWindow.on('ready-to-show', () => {
mainWindow?.show()
// 🚀 Mở DevTools khi sẵn sàng
mainWindow?.webContents.openDevTools({ mode: 'detach' })
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// Make the window always on top
mainWindow.setAlwaysOnTop(true, 'normal')
// mainWindow.webContents.openDevTools()
// Inspect element with shortcut
mainWindow.webContents.once('did-finish-load', () => {
const shortcut = 'Control+Shift+C'
mainWindow?.on('focus', () => {
globalShortcut.register(shortcut, () => {
const pos = screen.getCursorScreenPoint()
mainWindow?.webContents.inspectElement(pos.x, pos.y)
mainWindow?.webContents.focus() // optional: refocus back
})
})
mainWindow?.on('blur', () => {
globalShortcut.unregister(shortcut)
})
mainWindow?.on('closed', () => {
globalShortcut.unregister(shortcut)
})
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
// Khi bấm dấu X
mainWindow.on('close', (event) => {
if (!isQuiting) {
event.preventDefault()
mainWindow?.hide()
}
})
}
function createTray() {
const tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show',
click: () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
mainWindow.focus()
} else {
createWindow()
}
}
},
{
label: 'Quit',
click: () => {
isQuiting = true
app.quit()
}
}
@ -69,3 +163,57 @@ app.whenReady().then(() => {
app.on('will-quit', () => {
unregisterGlobalShortcuts()
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// 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 {
// 📦 Ghi dữ liệu mới
const newData = jsonContent
fs.writeFileSync(filePath, JSON.stringify(newData, null, 2), 'utf-8')
// Đăng ký lại global shortcuts
unregisterGlobalShortcuts()
registerGlobalShortcuts()
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
}
})

View File

@ -1,67 +0,0 @@
import { globalShortcut, clipboard, shell } from 'electron'
export interface IShortcut {
shortcut: string
links: string[]
}
// Danh sách shortcut mẫu
const shortcuts: IShortcut[] = [
{
links: [
'https://int.ipsupply.com.au/erptools/001_search-vpn?search=${query}',
'https://www.ebay.com/sch/i.html?_nkw=${query}&_sop=15'
],
shortcut: 'CommandOrControl+Shift+1'
},
{
links: ['https://esearch.danielvu.com?keyword=${query}'],
shortcut: 'CommandOrControl+Shift+2'
}
]
/**
* Mở tất cả link gắn với shortcut thay ${query} bằng clipboard text
*/
const handleOpenBrowserByLink = (data: IShortcut) => {
const text = clipboard.readText().trim()
if (!text) {
console.log(`[${new Date().toLocaleTimeString()}] Clipboard is empty.`)
return
}
const query = encodeURIComponent(text)
data.links.forEach((link) => {
const url = link.replace(/\$\{query\}/g, query) // Thay ${query} trong link
console.log(`[${new Date().toLocaleTimeString()}] Opening URL: ${url}`)
shell.openExternal(url) // Mở trong browser mặc định
})
}
/**
* Đăng tất cả global shortcut
*/
export function registerGlobalShortcuts() {
shortcuts.forEach((sc) => {
const ret = globalShortcut.register(sc.shortcut, () => {
console.log(`🚀 Triggered shortcut: ${sc.shortcut}`)
handleOpenBrowserByLink(sc)
})
if (!ret) {
console.log(`❌ Failed to register shortcut: ${sc.shortcut}`)
} else {
console.log(`✅ Registered shortcut: ${sc.shortcut}`)
}
})
}
/**
* Xóa tất cả global shortcut khi thoát app
*/
export function unregisterGlobalShortcuts() {
globalShortcut.unregisterAll()
console.log(' All global shortcuts unregistered.')
}

126
src/main/shotcut.ts Normal file
View File

@ -0,0 +1,126 @@
// shotcut.ts
import { globalShortcut, clipboard, shell, app } from 'electron'
import fs from 'fs'
import path from 'path'
import { v4 as uuid } from 'uuid'
export interface IShortcut {
id: string
shortcut: string
links: string[]
}
// Danh sách shortcut mặc định
export const defaultShortcuts: IShortcut[] = [
{
id: uuid(),
links: [
'https://int.ipsupply.com.au/erptools/001_search-vpn?search=${keyword}',
'https://www.ebay.com/sch/i.html?_nkw=${keyword}&_sop=15'
],
shortcut: 'CommandOrControl+Shift+1'
},
{
id: uuid(),
links: ['https://esearch.danielvu.com?keyword=${keyword}'],
shortcut: 'CommandOrControl+Shift+2'
}
]
// Lấy đường dẫn file config trong thư mục userData
const getConfigFilePath = () => {
return path.join(app.getPath('userData'), 'config-data.json')
}
/**
* Đm bảo file config tồn tại
* Nếu chưa , tạo mới với dữ liệu defaultShortcuts
*/
const ensureConfigFile = () => {
const filePath = getConfigFilePath()
if (!fs.existsSync(filePath)) {
try {
fs.writeFileSync(filePath, JSON.stringify(defaultShortcuts, null, 2), 'utf-8')
console.log(`📝 Created default config file at: ${filePath}`)
} catch (err) {
console.error('❌ Failed to create default config file:', err)
}
}
}
/**
* Đc danh sách shortcut từ file config
*/
const loadShortcutsFromConfig = (): IShortcut[] => {
const filePath = getConfigFilePath()
ensureConfigFile() // 🔹 Đảm bảo luôn có file trước khi đọc
try {
const rawData = fs.readFileSync(filePath, 'utf-8')
const shortcuts = JSON.parse(rawData) as IShortcut[]
if (!Array.isArray(shortcuts)) {
throw new Error('Config file format is invalid: must be an array')
}
return shortcuts
} catch (err) {
console.error('❌ Failed to read or parse config file:', err)
return []
}
}
/**
* Mở tất cả link gắn với shortcut thay ${query} bằng clipboard text
*/
const handleOpenBrowserByLink = (data: IShortcut) => {
const text = clipboard.readText().trim()
if (!text) {
console.log(`[${new Date().toLocaleTimeString()}] Clipboard is empty.`)
return
}
const query = encodeURIComponent(text)
data.links.forEach((link) => {
const url = link.replace(/\$\{keyword\}/g, query) // Thay ${query} trong link
console.log(`[${new Date().toLocaleTimeString()}] Opening URL: ${url}`)
shell.openExternal(url) // Mở trong browser mặc định
})
}
/**
* Đăng tất cả global shortcut từ file config
*/
export function registerGlobalShortcuts() {
const shortcuts = loadShortcutsFromConfig()
if (shortcuts.length === 0) {
console.log('⚠️ No shortcuts found to register.')
return
}
shortcuts.forEach((sc) => {
const ret = globalShortcut.register(sc.shortcut, () => {
console.log(`🚀 Triggered shortcut: ${sc.shortcut}`)
handleOpenBrowserByLink(sc)
})
if (!ret) {
console.log(`❌ Failed to register shortcut: ${sc.shortcut}`)
} else {
console.log(`✅ Registered shortcut: ${sc.shortcut}`)
}
})
}
/**
* Xóa tất cả global shortcut khi thoát app
*/
export function unregisterGlobalShortcuts() {
globalShortcut.unregisterAll()
console.log(' All global shortcuts unregistered.')
}

View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Shotcut</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/> -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

92
src/renderer/src/App.tsx Normal file
View File

@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Box, Container, LoadingOverlay, MantineProvider } from '@mantine/core'
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import { Suspense, useEffect, useState } from 'react'
import { ShotcutForm } from './components/ShotcutForm'
import { ShotcutList } from './components/ShotcutList'
import { theme } from './theme'
import { IShotcut } from './types'
import { getConfigFile, saveConfigFile } from './api/config'
function App(): React.JSX.Element {
const [loading, setLoading] = useState(true)
const [shotcuts, setShotcuts] = useState<IShotcut[]>([])
// Hàm lưu config
const saveConfig = async (newShortcuts: IShotcut[]) => {
const result = await saveConfigFile(newShortcuts)
if (!result.success) {
console.error('Failed to save config:', result.error)
} else {
console.log('✅ Config saved to', result.path)
}
}
const handleDelete = (shotcut: IShotcut) => {
const newShortcuts = shotcuts.filter((item) => item.id !== shotcut.id)
setShotcuts(newShortcuts)
saveConfig(newShortcuts)
}
// Gọi IPC để lấy dữ liệu config
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await getConfigFile()
console.log('Config data:', data)
if (data) {
setShotcuts(data)
} else {
setShotcuts([])
}
} catch (error) {
console.error('Error loading config file:', error)
setShotcuts([])
} finally {
setLoading(false)
}
}
fetchData()
}, [])
return (
<MantineProvider theme={theme}>
<Suspense
fallback={
<LoadingOverlay
visible={true}
zIndex={1000}
overlayProps={{ radius: 'sm', blur: 1 }}
/>
}
>
<Container fluid mt={'lg'}>
<ShotcutForm
onSubmit={(newItem) => {
const updated = [newItem, ...shotcuts]
setShotcuts(updated)
saveConfig(updated)
}}
/>
<Box py={'lg'}>
<ShotcutList shotcuts={shotcuts} onDelete={handleDelete} />
</Box>
<LoadingOverlay
visible={loading}
zIndex={1000}
overlayProps={{ radius: 'sm', blur: 1 }}
/>
</Container>
</Suspense>
</MantineProvider>
)
}
export default App

View File

@ -0,0 +1,10 @@
import { IShotcut } from '@renderer/types'
// src/api/config.ts
export const getConfigFile = async () => {
return await window.electron.ipcRenderer.invoke('get-config-file')
}
export const saveConfigFile = async (data: IShotcut[]) => {
return await window.electron.ipcRenderer.invoke('save-config-file', data)
}

View File

@ -0,0 +1,54 @@
@keyframes borderPulse {
0% {
border-color: #e03131;
}
50% {
border-color: black;
}
100% {
border-color: #e03131;
}
}
.item-card {
animation: borderPulse 1s infinite;
border: 4px solid #e03131;
border-radius: 4px;
transition: border-color 0.3s ease;
padding: 10px;
}
.bold-text {
font-weight: 600;
color: #495057;
}
.item-card p {
font-size: 14px !important;
}
.text-overflow-1-line {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.text-overflow-1-line:hover {
text-decoration: underline;
cursor: pointer;
}
.item-picture {
height: auto;
width: 100%;
max-height: 100px;
border-radius: 4px;
object-fit: contain;
}
@media only screen and (max-width: 768px) {
.item-picture {
object-fit: cover;
}
}

View File

@ -0,0 +1,89 @@
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: 10, // default timeout 30s
delay_hide: 10
},
validate: {
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null),
delay_hide: (value) => (value <= 0 ? 'Delay 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,
delay_hide: form.values.delay_hide
}
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')
if (!data) return
form.setValues({ timeout: data?.noti_timeout, delay_hide: data.delay_hide })
} 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')}
/>
<NumberInput
label="Delay (seconds)"
placeholder="Enter timeout in seconds"
min={1}
{...form.getInputProps('delay_hide')}
/>
<Button type="submit" fullWidth>
Save
</Button>
</Stack>
</form>
</Modal>
)
}

View File

@ -0,0 +1,127 @@
'use client'
import { ActionIcon, Button, Code, Group, Paper, Stack, Text, TextInput } from '@mantine/core'
import { useForm, zodResolver } from '@mantine/form'
import { IShotcut } from '@renderer/types'
import { IconPlus, IconX } from '@tabler/icons-react'
import { v4 as uuid } from 'uuid'
import * as z from 'zod'
// Modifier keys
const MODIFIERS = ['Ctrl', 'Control', 'Cmd', 'Command', 'Alt', 'Option', 'Shift']
// Last key regex
const keyRegex = /^([A-Z0-9]|F[1-9]|F1[0-2]|Enter|Tab)$/i
// Zod schema
const ShotcutSchema = z.object({
shortcut: z
.string()
.nonempty('Shortcut is required')
.refine((val) => {
const parts = val.split('+').map((p) => p.trim())
if (parts.length < 2) return false
const last = parts[parts.length - 1]
const modifiers = parts.slice(0, -1)
return modifiers.every((m) => MODIFIERS.includes(m)) && keyRegex.test(last)
}, 'Invalid shortcut format. Example: Ctrl+Shift+1'),
links: z
.array(
z
.string()
.nonempty('Link cannot be empty')
.refine((val) => {
const temp = val.replaceAll('{{keyword}}', 'test')
try {
new URL(temp)
return true
} catch {
return false
}
}, 'Invalid URL. Can include {{keyword}}')
)
.min(1, 'At least one link is required')
})
type ShotcutFormValues = z.infer<typeof ShotcutSchema>
interface ShotcutFormProps {
onSubmit: (shotcut: IShotcut) => void
}
export function ShotcutForm({ onSubmit }: ShotcutFormProps) {
const form = useForm<ShotcutFormValues>({
initialValues: { shortcut: '', links: [''] },
validate: zodResolver(ShotcutSchema)
})
const addLink = () => form.insertListItem('links', '')
const removeLink = (index: number) => form.removeListItem('links', index)
const handleSubmit = (values: ShotcutFormValues) => {
onSubmit({ ...values, id: uuid() })
form.reset()
}
return (
<Paper shadow="sm" radius="md" p="md" withBorder maw={600} mx="auto">
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Text size="lg" fw={600}>
Create New Shortcut
</Text>
<TextInput
label="Shortcut"
placeholder="Ctrl+Shift+1"
{...form.getInputProps('shortcut')}
size="sm"
/>
<Stack>
<Group align="center">
<Text size="sm">Links</Text>
<Text size="xs" c="dimmed">
Use <Code>{'{keyword}'}</Code> as placeholder
</Text>
</Group>
{form.values.links.map((link, index) => (
<Group key={index} align="center">
<TextInput
placeholder="https://google.com/search?q={keyword}"
{...form.getInputProps(`links.${index}`)}
size="sm"
style={{ flex: 1 }}
/>
{form.values.links.length > 1 && (
<ActionIcon
variant="outline"
color="red"
size="sm"
onClick={() => removeLink(index)}
>
<IconX size={14} />
</ActionIcon>
)}
</Group>
))}
<Button
type="button"
variant="outline"
onClick={addLink}
size="sm"
leftSection={<IconPlus size={14} />}
>
Add Link
</Button>
<Button type="submit" size="sm">
Create Shortcut
</Button>
</Stack>
</Stack>
</form>
</Paper>
)
}

View File

@ -0,0 +1,105 @@
'use client'
import { useState } from 'react'
import { ActionIcon, Badge, Card, Group, Stack, Text, Modal, Button, Flex } from '@mantine/core'
import { IShotcut } from '@renderer/types'
import { IconExternalLink, IconTrash } from '@tabler/icons-react'
interface ShotcutListProps {
shotcuts: IShotcut[]
onDelete: (shotcut: IShotcut) => void
}
export function ShotcutList({ shotcuts, onDelete }: ShotcutListProps) {
const [opened, setOpened] = useState(false)
const [selectedShotcut, setSelectedShotcut] = useState<IShotcut | null>(null)
const openConfirmModal = (shotcut: IShotcut) => {
setSelectedShotcut(shotcut)
setOpened(true)
}
const handleConfirmDelete = () => {
if (selectedShotcut) {
onDelete(selectedShotcut)
setSelectedShotcut(null)
setOpened(false)
}
}
if (shotcuts.length === 0) {
return (
<Card shadow="sm" radius="md" p="md" withBorder>
<Text ta="center" c="dimmed">
No shortcuts yet. Create your first shortcut!
</Text>
</Card>
)
}
return (
<Stack>
<Text size="xl" fw={600}>
Shortcut List
</Text>
{shotcuts.map((shotcut, index) => (
<Card key={index} shadow="sm" radius="md" p="md" withBorder>
{/* Header */}
<Group mb="sm">
<Text size="lg" fw={500}>
{shotcut.shortcut}
</Text>
<ActionIcon
variant="outline"
color="red"
onClick={() => openConfirmModal(shotcut)}
title="Delete shortcut"
>
<IconTrash size={16} />
</ActionIcon>
</Group>
{/* Content */}
<Stack>
<Text size="sm" c="dimmed">
{shotcut.links.length} link(s)
</Text>
{/* Link badges */}
<Group>
{shotcut.links.map((link, linkIndex) => (
<Badge
key={linkIndex}
variant="outline"
radius="sm"
style={{ cursor: 'pointer' }}
onClick={() => window.open(link, '_blank')}
leftSection={<IconExternalLink size={14} />}
>
{link}
</Badge>
))}
</Group>
</Stack>
</Card>
))}
{/* Confirm Modal */}
<Modal opened={opened} onClose={() => setOpened(false)} title="Confirm Delete" centered>
<Text mb="md">
Are you sure you want to delete shortcut{' '}
<strong>{selectedShotcut?.shortcut}</strong>?
</Text>
<Flex justify={'flex-end'} gap={'sm'}>
<Button variant="default" onClick={() => setOpened(false)}>
Cancel
</Button>
<Button color="red" onClick={handleConfirmDelete}>
Delete
</Button>
</Flex>
</Modal>
</Stack>
)
}

1
src/renderer/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

7
src/renderer/src/global.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
export {}
declare global {
interface Window {
disableSound: any
}
}

11
src/renderer/src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import './assets/main.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)

View File

@ -0,0 +1,6 @@
import Pusher from 'pusher-js'
export const pusher = new Pusher(import.meta.env.VITE_PUSHER_APP_KEY, {
cluster: 'ap4'
// encrypted: true
})

View File

@ -0,0 +1,5 @@
import { createTheme } from '@mantine/core'
import { themeToVars } from '@mantine/vanilla-extract'
export const theme = createTheme({})
export const vars = themeToVars(theme)

View File

@ -0,0 +1,5 @@
export interface IShotcut {
id: string
shortcut: string
links: string[]
}

View File

@ -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}`
}
}