fix build env
This commit is contained in:
parent
42aeeb3e8d
commit
375725c7d7
|
|
@ -0,0 +1 @@
|
||||||
|
VITE_API_KEY = ''
|
||||||
|
|
@ -13,6 +13,7 @@ dist-electron
|
||||||
release
|
release
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,34 @@
|
||||||
// @see - https://www.electron.build/configuration/configuration
|
// @see - https://www.electron.build/configuration/configuration
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
|
$schema: 'https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json',
|
||||||
"appId": "YourAppID",
|
appId: 'b1730d2e-6f3c-4f92-9f4a-9f8e918b40d2',
|
||||||
"asar": true,
|
asar: true,
|
||||||
"productName": "YourAppName",
|
productName: 'Zulip messages',
|
||||||
"directories": {
|
directories: {
|
||||||
"output": "release/${version}"
|
output: 'release/${version}',
|
||||||
},
|
},
|
||||||
"files": [
|
files: ['dist', 'dist-electron'],
|
||||||
"dist",
|
mac: {
|
||||||
"dist-electron"
|
target: ['dmg'],
|
||||||
],
|
artifactName: '${productName}-Mac-${version}-Installer.${ext}',
|
||||||
"mac": {
|
},
|
||||||
"target": [
|
win: {
|
||||||
"dmg"
|
target: [
|
||||||
],
|
{
|
||||||
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
|
target: 'nsis',
|
||||||
},
|
arch: ['x64'],
|
||||||
"win": {
|
},
|
||||||
"target": [
|
],
|
||||||
{
|
artifactName: '${productName}-Windows-${version}-Setup.${ext}',
|
||||||
"target": "nsis",
|
},
|
||||||
"arch": [
|
nsis: {
|
||||||
"x64"
|
oneClick: false,
|
||||||
]
|
perMachine: false,
|
||||||
}
|
allowToChangeInstallationDirectory: true,
|
||||||
],
|
deleteAppDataOnUninstall: false,
|
||||||
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
|
},
|
||||||
},
|
linux: {
|
||||||
"nsis": {
|
target: ['AppImage'],
|
||||||
"oneClick": false,
|
artifactName: '${productName}-Linux-${version}.${ext}',
|
||||||
"perMachine": false,
|
},
|
||||||
"allowToChangeInstallationDirectory": true,
|
|
||||||
"deleteAppDataOnUninstall": false
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": [
|
|
||||||
"AppImage"
|
|
||||||
],
|
|
||||||
"artifactName": "${productName}-Linux-${version}.${ext}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
232
electron/main.ts
232
electron/main.ts
|
|
@ -1,17 +1,13 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { config } from "dotenv";
|
import { app, BrowserWindow, ipcMain, Menu, Notification, screen } from 'electron';
|
||||||
import { app, BrowserWindow, ipcMain, Notification, screen } from "electron";
|
import path from 'node:path';
|
||||||
import path from "node:path";
|
import { fileURLToPath } from 'node:url';
|
||||||
import { fileURLToPath } from "node:url";
|
import { io } from 'socket.io-client';
|
||||||
import { io } from "socket.io-client";
|
import { addEmail, deleteEmail, fetchEmails, fetchMessages } from '../src/apis';
|
||||||
import { WebSocket } from "ws";
|
import { createMailWindow } from './windows/mails.window';
|
||||||
import { addEmail, deleteEmail, fetchEmails, fetchMessages } from "../src/apis";
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// dot env
|
|
||||||
config();
|
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
//
|
//
|
||||||
// ├─┬─┬ dist
|
// ├─┬─┬ dist
|
||||||
|
|
@ -21,174 +17,142 @@ config();
|
||||||
// │ │ ├── main.js
|
// │ │ ├── main.js
|
||||||
// │ │ └── preload.mjs
|
// │ │ └── preload.mjs
|
||||||
// │
|
// │
|
||||||
process.env.APP_ROOT = path.join(__dirname, "..");
|
process.env.APP_ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
let newWin: BrowserWindow | null = null;
|
|
||||||
|
|
||||||
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
|
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
|
||||||
export const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"];
|
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'];
|
||||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
|
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron');
|
||||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
|
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
|
||||||
|
|
||||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST;
|
||||||
? path.join(process.env.APP_ROOT, "public")
|
|
||||||
: RENDERER_DIST;
|
|
||||||
|
|
||||||
let win: BrowserWindow | null;
|
let win: BrowserWindow | null;
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(null);
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Lấy thông tin tất cả các màn hình
|
// Lấy thông tin tất cả các màn hình
|
||||||
const displays = screen.getAllDisplays();
|
const displays = screen.getAllDisplays();
|
||||||
|
|
||||||
// Lấy vị trí con trỏ chuột
|
// Lấy vị trí con trỏ chuột
|
||||||
const cursorPoint = screen.getCursorScreenPoint();
|
const cursorPoint = screen.getCursorScreenPoint();
|
||||||
|
|
||||||
// Tìm màn hình có chứa con trỏ chuột
|
// Tìm màn hình có chứa con trỏ chuột
|
||||||
let display = displays.find((display) => {
|
let display = displays.find((display) => {
|
||||||
const { x, y, width, height } = display.bounds;
|
const { x, y, width, height } = display.bounds;
|
||||||
return (
|
return cursorPoint.x >= x && cursorPoint.x <= x + width && cursorPoint.y >= y && cursorPoint.y <= y + height;
|
||||||
cursorPoint.x >= x &&
|
});
|
||||||
cursorPoint.x <= x + width &&
|
|
||||||
cursorPoint.y >= y &&
|
|
||||||
cursorPoint.y <= y + height
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Nếu không tìm thấy màn hình chứa con trỏ, sử dụng màn hình chính
|
// Nếu không tìm thấy màn hình chứa con trỏ, sử dụng màn hình chính
|
||||||
if (!display) {
|
if (!display) {
|
||||||
display = screen.getPrimaryDisplay();
|
display = screen.getPrimaryDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { width, height } = display.workAreaSize;
|
const { width, height } = display.workAreaSize;
|
||||||
|
|
||||||
// Vị trí cửa sổ ở góc phải dưới của màn hình đã chọn
|
// Vị trí cửa sổ ở góc phải dưới của màn hình đã chọn
|
||||||
|
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
width: 600,
|
width: 600,
|
||||||
height: 200,
|
height: 200,
|
||||||
x: 0, // Đặt cửa sổ ở góc phải
|
x: 0, // Đặt cửa sổ ở góc phải
|
||||||
y: 0, // Đặt cửa sổ ở góc dưới
|
y: 0, // Đặt cửa sổ ở góc dưới
|
||||||
alwaysOnTop: true, // Cửa sổ luôn nằm trên các cửa sổ khác
|
alwaysOnTop: true, // Cửa sổ luôn nằm trên các cửa sổ khác
|
||||||
resizable: true, // Không cho phép thay đổi kích thước
|
resizable: true, // Không cho phép thay đổi kích thước
|
||||||
icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
|
icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.mjs"),
|
preload: path.join(__dirname, 'preload.mjs'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test active push message to Renderer-process.
|
// Test active push message to Renderer-process.
|
||||||
win.webContents.on("did-finish-load", () => {
|
win.webContents.on('did-finish-load', () => {
|
||||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
win?.webContents.send('main-process-message', new Date().toLocaleString());
|
||||||
});
|
});
|
||||||
|
|
||||||
win.setPosition(width - 600, height - 200);
|
win.setPosition(width - 600, height - 200);
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) {
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL);
|
win.loadURL(VITE_DEV_SERVER_URL);
|
||||||
} else {
|
} else {
|
||||||
// win.loadFile('dist/index.html')
|
// win.loadFile('dist/index.html')
|
||||||
win.loadFile(path.join(RENDERER_DIST, "index.html"));
|
win.loadFile(path.join(RENDERER_DIST, 'index.html'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// 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
|
// for applications and their menu bar to stay active until the user quits
|
||||||
// explicitly with Cmd + Q.
|
// explicitly with Cmd + Q.
|
||||||
app.on("window-all-closed", () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
win = null;
|
win = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on('activate', () => {
|
||||||
// On OS X it's common to re-create a window in the app when the
|
// On OS X it's common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow);
|
||||||
|
|
||||||
// IPC Main Events
|
// IPC Main Events
|
||||||
ipcMain.on("open-devtools", (event) => {
|
ipcMain.on('open-devtools', (event) => {
|
||||||
const webContents = event.sender;
|
const webContents = event.sender;
|
||||||
webContents.openDevTools({ mode: "detach" });
|
webContents.openDevTools({ mode: 'detach' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Xử lý connect socket
|
// Xử lý connect socket
|
||||||
ipcMain.handle("connect-socket", async (_) => {
|
ipcMain.handle('connect-socket', async (_) => {
|
||||||
const socket = io("https://zulip.ipsupply.com.au", {
|
const socket = io('https://zulip.ipsupply.com.au', {
|
||||||
path: "/apac-custom/socket.io",
|
path: '/apac-custom/socket.io',
|
||||||
secure: true,
|
secure: true,
|
||||||
query: { token: process.env.API_KEY },
|
query: { token: import.meta.env.VITE_API_KEY },
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!socket.connected) {
|
if (!socket.connected) {
|
||||||
socket.connect();
|
socket.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on('connect', () => {
|
||||||
console.log(socket.connected); // true
|
console.log(socket.connected); // true
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("newNote", (data) => {
|
socket.on('newNote', (data) => {
|
||||||
win?.webContents.send("newNote", data);
|
win?.webContents.send('newNote', data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("fetchMessages", async () => {
|
ipcMain.handle('fetchMessages', async () => {
|
||||||
return await fetchMessages();
|
return await fetchMessages();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("fetchEmails", async () => {
|
ipcMain.handle('fetchEmails', async () => {
|
||||||
return await fetchEmails();
|
return await fetchEmails();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("open-new-window", async () => {
|
ipcMain.handle('open-new-window', async () => {
|
||||||
createMailWindow();
|
createMailWindow({ RENDERER_DIST });
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("add-email", async (_, email: string) => {
|
ipcMain.handle('add-email', async (_, email: string) => {
|
||||||
return addEmail(email);
|
return addEmail(email);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("del-email", async (_, id: number) => {
|
ipcMain.handle('del-email', async (_, id: number) => {
|
||||||
return deleteEmail(id);
|
return deleteEmail(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("show-notification", async (_, { title, body }) => {
|
ipcMain.handle('show-notification', async (_, { title, body }) => {
|
||||||
const notification = new Notification({
|
const notification = new Notification({
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
silent: false,
|
silent: false,
|
||||||
});
|
});
|
||||||
notification.show();
|
notification.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Funtions
|
|
||||||
export function createMailWindow() {
|
|
||||||
newWin = new BrowserWindow({
|
|
||||||
width: 600,
|
|
||||||
height: 400,
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, "preload.mjs"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Nếu đang phát triển, sử dụng HashRouter trong URL
|
|
||||||
const url = process.env.VITE_DEV_SERVER_URL
|
|
||||||
? `${process.env.VITE_DEV_SERVER_URL}/#/mails` // URL với HashRouter
|
|
||||||
: path.join(RENDERER_DIST, "index.html"); // Nếu build, dùng đúng path
|
|
||||||
|
|
||||||
newWin.loadURL(url);
|
|
||||||
|
|
||||||
// 👉 Mở DevTools sau khi load
|
|
||||||
newWin.webContents.openDevTools();
|
|
||||||
|
|
||||||
newWin.on("closed", () => {
|
|
||||||
newWin = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
process.env.APP_ROOT = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
export function createMailWindow({ RENDERER_DIST }: { RENDERER_DIST: string }) {
|
||||||
|
let newWin: BrowserWindow | null = new BrowserWindow({
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.mjs'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = process.env.VITE_DEV_SERVER_URL ? `${process.env.VITE_DEV_SERVER_URL}/#/mails` : `file://${path.join(RENDERER_DIST, 'index.html')}#/mails`;
|
||||||
|
|
||||||
|
newWin.loadURL(url);
|
||||||
|
|
||||||
|
newWin.on('closed', () => {
|
||||||
|
newWin = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newWin;
|
||||||
|
}
|
||||||
|
|
@ -16,14 +16,12 @@
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"dotenv": "^16.5.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.5.3",
|
"react-router-dom": "^7.5.3",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"utf-8-validate": "^6.0.5",
|
"utf-8-validate": "^6.0.5",
|
||||||
"ws": "^8.18.2",
|
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -3916,18 +3914,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
|
||||||
"version": "16.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
|
||||||
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://dotenvx.com"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv-expand": {
|
"node_modules/dotenv-expand": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
|
||||||
|
|
@ -8228,27 +8214,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.18.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
|
||||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xmlbuilder": {
|
"node_modules/xmlbuilder": {
|
||||||
"version": "15.1.1",
|
"version": "15.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,12 @@
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"bufferutil": "^4.0.9",
|
"bufferutil": "^4.0.9",
|
||||||
"dotenv": "^16.5.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.5.3",
|
"react-router-dom": "^7.5.3",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"utf-8-validate": "^6.0.5",
|
"utf-8-validate": "^6.0.5",
|
||||||
"ws": "^8.18.2",
|
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,55 @@
|
||||||
import { config } from "dotenv";
|
import axios from '../instants/axios';
|
||||||
import axios from "../instants/axios";
|
|
||||||
|
|
||||||
config();
|
const API_KEY = import.meta.env.VITE_API_KEY;
|
||||||
|
|
||||||
const API_KEY = process.env.API_KEY;
|
|
||||||
|
|
||||||
export const getAllNote = async () => {
|
export const getAllNote = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: "GET",
|
method: 'GET',
|
||||||
url: "getAllNotes",
|
url: 'getAllNotes',
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchMessages(onError?: (error: unknown) => void) {
|
export async function fetchMessages(onError?: (error: unknown) => void) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`getAllNotes?key=${API_KEY}`);
|
const response = await axios.get(`getAllNotes?key=${API_KEY}`);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching notes:", error);
|
console.error('Error fetching notes:', error);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchEmails(onError?: (error: unknown) => void) {
|
export async function fetchEmails(onError?: (error: unknown) => void) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`emails?key=${API_KEY}`);
|
const response = await axios.get(`emails?key=${API_KEY}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching emails:", error);
|
console.error('Error fetching emails:', error);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addEmail(
|
export async function addEmail(email: string, onError?: (error: unknown) => void) {
|
||||||
email: string,
|
try {
|
||||||
onError?: (error: unknown) => void
|
const response = await axios.post(`add-email`, {
|
||||||
) {
|
email: email,
|
||||||
try {
|
key: API_KEY,
|
||||||
const response = await axios.post(`add-email`, {
|
});
|
||||||
email: email,
|
|
||||||
key: API_KEY,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error add email:", error);
|
console.error('Error add email:', error);
|
||||||
onError?.(error);
|
onError?.(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEmail(id: number) {
|
export async function deleteEmail(id: number) {
|
||||||
await axios.delete(`delete-email/${id}?key=${API_KEY}`);
|
await axios.delete(`delete-email/${id}?key=${API_KEY}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,28 @@
|
||||||
import { Button, Modal, Text } from "@mantine/core";
|
import { Button, Modal, Text } from '@mantine/core';
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
interface ConfirmModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
message: string;
|
message: string;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfirmModal({
|
export default function ConfirmModal({ opened, title = 'Xác nhận', message, onConfirm, onCancel }: ConfirmModalProps) {
|
||||||
opened,
|
return (
|
||||||
title = "Xác nhận",
|
<Modal opened={opened} onClose={onCancel} title={title} centered>
|
||||||
message,
|
<Text size="md" mb="xs">
|
||||||
onConfirm,
|
{message}
|
||||||
onCancel,
|
</Text>
|
||||||
}: ConfirmModalProps) {
|
|
||||||
return (
|
|
||||||
<Modal opened={opened} onClose={onCancel} title={title} centered>
|
|
||||||
<Text size="xs" mb="md">
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||||
<Button variant="default" onClick={onCancel}>
|
<Button size="xs" variant="default" onClick={onCancel}>
|
||||||
No
|
No
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="red" onClick={onConfirm}>
|
<Button size="xs" color="red" onClick={onConfirm}>
|
||||||
Yes
|
Yes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,62 @@
|
||||||
import { ActionIcon, Box, Paper, Textarea, Tooltip } from "@mantine/core";
|
import { ActionIcon, Box, Paper, Textarea, Tooltip } from '@mantine/core';
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconEye, IconEyeOff, IconTrash } from "@tabler/icons-react";
|
import { IconEye, IconEyeOff, IconTrash } from '@tabler/icons-react';
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
export interface IMessageProps {
|
export interface IMessageProps {
|
||||||
data: IMessage;
|
data: IMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message({ data }: IMessageProps) {
|
export default function Message({ data }: IMessageProps) {
|
||||||
const [show, { toggle }] = useDisclosure(false);
|
const [show, { toggle }] = useDisclosure(false);
|
||||||
|
|
||||||
const messageContent = useMemo(() => {
|
const [animation, setAnimation] = useState(false);
|
||||||
return show ? data.message : data.message.replace(/./g, "•");
|
|
||||||
}, [data.message, show]);
|
|
||||||
|
|
||||||
return (
|
const messageContent = useMemo(() => {
|
||||||
<Paper
|
return show ? data.message : data.message.replace(/./g, '•');
|
||||||
key={data.message}
|
}, [data.message, show]);
|
||||||
shadow="xs"
|
|
||||||
radius="md"
|
// Tắt hiệu ứng nhấp nháy sau 1 giây
|
||||||
p="xs"
|
useEffect(() => {
|
||||||
withBorder
|
if (!animation) return;
|
||||||
className="flex items-center gap-3 w-full"
|
|
||||||
>
|
const timer = setTimeout(() => {
|
||||||
<div className="flex items-center gap-2">
|
setAnimation(false);
|
||||||
<Textarea
|
}, 5000);
|
||||||
defaultValue={messageContent}
|
return () => clearTimeout(timer);
|
||||||
value={messageContent}
|
}, [animation]);
|
||||||
className="flex-1"
|
|
||||||
variant="unstyled"
|
useEffect(() => {
|
||||||
/>
|
const showAnimation = new Date().getTime() - new Date(data.time).getTime() <= 3000;
|
||||||
<Box className="flex gap-1">
|
|
||||||
<Tooltip label="View">
|
setAnimation(showAnimation);
|
||||||
<ActionIcon onClick={toggle} variant="light" size="sm">
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
{show ? <IconEyeOff size={16} /> : <IconEye size={16} />}
|
}, []);
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
return (
|
||||||
<Tooltip label="Delete">
|
<Paper
|
||||||
<ActionIcon variant="light" size="sm" color="red">
|
key={data.message}
|
||||||
<IconTrash size={16} />
|
shadow="xs"
|
||||||
</ActionIcon>
|
radius="md"
|
||||||
</Tooltip>
|
p="xs"
|
||||||
</Box>
|
withBorder
|
||||||
</div>
|
className={`flex items-center gap-3 w-full transition-all duration-300 ${animation ? 'animate-pulse !bg-blue-100' : ''}`}
|
||||||
</Paper>
|
>
|
||||||
);
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<Textarea defaultValue={messageContent} value={messageContent} className="flex-1" variant="unstyled" readOnly />
|
||||||
|
<Box className="flex gap-1">
|
||||||
|
<Tooltip label="View">
|
||||||
|
<ActionIcon onClick={toggle} variant="light" size="sm">
|
||||||
|
{show ? <IconEyeOff size={16} /> : <IconEye size={16} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Delete">
|
||||||
|
<ActionIcon variant="light" size="sm" color="red">
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,160 @@
|
||||||
import {
|
import { ActionIcon, Box, Button, Container, LoadingOverlay, Paper, ScrollArea, Text, TextInput } from '@mantine/core';
|
||||||
ActionIcon,
|
import { useForm, zodResolver } from '@mantine/form';
|
||||||
Box,
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
Button,
|
import { IconMail, IconTrash } from '@tabler/icons-react'; // Icon xóa
|
||||||
Container,
|
import { nanoid } from 'nanoid'; // Để tạo ID cho mỗi email mới
|
||||||
LoadingOverlay,
|
import { useEffect, useRef, useState } from 'react';
|
||||||
Paper,
|
import { z } from 'zod';
|
||||||
ScrollArea,
|
import { ConfirmModal } from '../components';
|
||||||
Text,
|
import { showNotification } from '../ultils/fn';
|
||||||
TextInput,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useForm, zodResolver } from "@mantine/form";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
|
||||||
import { IconMail, IconTrash } from "@tabler/icons-react"; // Icon xóa
|
|
||||||
import { nanoid } from "nanoid"; // Để tạo ID cho mỗi email mới
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { ConfirmModal } from "../components";
|
|
||||||
import { showNotification } from "../ultils/fn";
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
email: z.string().email("Invalid email"),
|
email: z.string().email('Invalid email'),
|
||||||
});
|
});
|
||||||
|
|
||||||
function MailsPage() {
|
function MailsPage() {
|
||||||
const viewport = useRef<HTMLDivElement>(null);
|
const viewport = useRef<HTMLDivElement>(null);
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [clickEmail, setClickEmail] = useState<IEmail | null>(null);
|
const [clickEmail, setClickEmail] = useState<IEmail | null>(null);
|
||||||
const [emails, setEmails] = useState<IEmail[]>([]); // Dùng để lưu danh sách email
|
const [emails, setEmails] = useState<IEmail[]>([]); // Dùng để lưu danh sách email
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
email: '',
|
||||||
},
|
},
|
||||||
validate: zodResolver(schema),
|
validate: zodResolver(schema),
|
||||||
});
|
|
||||||
|
|
||||||
const scrollToBottom = () =>
|
|
||||||
viewport.current!.scrollTo({
|
|
||||||
top: viewport.current!.scrollHeight,
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hàm thêm email vào danh sách
|
const scrollToBottom = () =>
|
||||||
const handleAddEmail = async ({ email }: { email: string }) => {
|
viewport.current!.scrollTo({
|
||||||
if (email.trim() !== "") {
|
top: viewport.current!.scrollHeight,
|
||||||
try {
|
behavior: 'smooth',
|
||||||
setLoading(true);
|
|
||||||
const response = await window.ipcRenderer.invoke("add-email", email);
|
|
||||||
|
|
||||||
if (!response) return;
|
|
||||||
|
|
||||||
console.log("%csrc/pages/mails.tsx:40 {response}", "color: #007acc;", {
|
|
||||||
response,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetchEmails();
|
// Hàm thêm email vào danh sách
|
||||||
|
const handleAddEmail = async ({ email }: { email: string }) => {
|
||||||
|
if (email.trim() !== '') {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await window.ipcRenderer.invoke('add-email', email);
|
||||||
|
|
||||||
showNotification("Thành công", "Đã thêm email mới");
|
if (!response) return;
|
||||||
|
|
||||||
scrollToBottom();
|
console.log('%csrc/pages/mails.tsx:40 {response}', 'color: #007acc;', {
|
||||||
setClickEmail(null);
|
response,
|
||||||
form.reset();
|
});
|
||||||
} catch (error) {
|
|
||||||
console.log("%csrc/pages/mails.tsx:65 error", "color: #007acc;", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteEmail = async (id: number | undefined) => {
|
await fetchEmails();
|
||||||
try {
|
|
||||||
if (!id) return;
|
|
||||||
setLoading(true);
|
|
||||||
await window.ipcRenderer.invoke("del-email", id);
|
|
||||||
|
|
||||||
await fetchEmails();
|
showNotification('Thành công', 'Đã thêm email mới');
|
||||||
close();
|
|
||||||
|
|
||||||
showNotification("Thông báo", "Đã xóa email");
|
scrollToBottom();
|
||||||
} catch (error) {
|
setClickEmail(null);
|
||||||
console.log("%csrc/pages/mails.tsx:88 error", "color: #007acc;", error);
|
form.reset();
|
||||||
} finally {
|
} catch (error) {
|
||||||
setLoading(false);
|
console.log('%csrc/pages/mails.tsx:65 error', 'color: #007acc;', error);
|
||||||
}
|
} finally {
|
||||||
};
|
setLoading(false);
|
||||||
|
}
|
||||||
const fetchEmails = async () => {
|
}
|
||||||
try {
|
|
||||||
await window.ipcRenderer.invoke("connect-socket");
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const mails = await window.ipcRenderer.invoke("fetchEmails");
|
|
||||||
|
|
||||||
console.log("%csrc/App.tsx:29 {mails}", "color: #007acc;", {
|
|
||||||
mails,
|
|
||||||
});
|
|
||||||
|
|
||||||
setEmails(mails);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to connect socket", error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchEmails();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
// window.ipcRenderer.removeListener("new-note", handleNewNote);
|
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
const handleDeleteEmail = async (id: number | undefined) => {
|
||||||
<Container style={{ paddingTop: 20 }} className="overflow-hidden">
|
try {
|
||||||
<Paper shadow="xs" p="md" style={{ marginBottom: 20 }}>
|
if (!id) return;
|
||||||
<form onSubmit={form.onSubmit(handleAddEmail)}>
|
setLoading(true);
|
||||||
<TextInput
|
await window.ipcRenderer.invoke('del-email', id);
|
||||||
{...form.getInputProps("email")}
|
|
||||||
label="Enter Email"
|
|
||||||
placeholder="example@example.com"
|
|
||||||
/>
|
|
||||||
<Button type="submit" fullWidth style={{ marginTop: 10 }}>
|
|
||||||
Add Email
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<ScrollArea viewportRef={viewport} h={200} type="auto" pb={"lg"}>
|
await fetchEmails();
|
||||||
<Box
|
close();
|
||||||
className="flex flex-col gap-4"
|
|
||||||
size="sm"
|
|
||||||
style={{ marginTop: 20 }}
|
|
||||||
>
|
|
||||||
{emails.length > 0 ? (
|
|
||||||
emails.map((item) => (
|
|
||||||
<Box key={nanoid()} className="flex items-center">
|
|
||||||
<ActionIcon size={"md"} variant="outline" className="mr-3">
|
|
||||||
<IconMail size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
<Text style={{ flexGrow: 1 }}>{item.email}</Text>
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
setClickEmail(item);
|
|
||||||
open();
|
|
||||||
}}
|
|
||||||
color="red"
|
|
||||||
size="md"
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Box>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Text className="!text-gray-500">No emails added yet.</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<LoadingOverlay visible={loading} />
|
showNotification('Thông báo', 'Đã xóa email');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('%csrc/pages/mails.tsx:88 error', 'color: #007acc;', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<ConfirmModal
|
const fetchEmails = async () => {
|
||||||
title="Warning"
|
try {
|
||||||
message="Caaj"
|
await window.ipcRenderer.invoke('connect-socket');
|
||||||
opened={opened}
|
|
||||||
onCancel={close}
|
setLoading(true);
|
||||||
onConfirm={() => handleDeleteEmail(clickEmail?.id)}
|
|
||||||
/>
|
const mails = await window.ipcRenderer.invoke('fetchEmails');
|
||||||
</Container>
|
|
||||||
);
|
console.log('%csrc/App.tsx:29 {mails}', 'color: #007acc;', {
|
||||||
|
mails,
|
||||||
|
});
|
||||||
|
|
||||||
|
setEmails(mails);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect socket', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEmails();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// window.ipcRenderer.removeListener("new-note", handleNewNote);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={{ paddingTop: 20 }} className="overflow-hidden">
|
||||||
|
<Paper shadow="xs" p="md" style={{ marginBottom: 20 }}>
|
||||||
|
<form onSubmit={form.onSubmit(handleAddEmail)}>
|
||||||
|
<TextInput {...form.getInputProps('email')} label="Enter Email" placeholder="example@example.com" />
|
||||||
|
<Button type="submit" fullWidth style={{ marginTop: 10 }}>
|
||||||
|
Add Email
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<ScrollArea viewportRef={viewport} h={200} type="auto" pb={'lg'}>
|
||||||
|
<Box className="flex flex-col gap-4" size="sm" style={{ marginTop: 20 }}>
|
||||||
|
{emails.length > 0 ? (
|
||||||
|
emails.map((item) => (
|
||||||
|
<Box key={nanoid()} className="flex items-center">
|
||||||
|
<ActionIcon size={'md'} variant="outline" className="mr-3">
|
||||||
|
<IconMail size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Text style={{ flexGrow: 1 }}>{item.email}</Text>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => {
|
||||||
|
setClickEmail(item);
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
size="md"
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className="!text-gray-500">No emails added yet.</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<LoadingOverlay visible={loading} />
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
title="Warning"
|
||||||
|
message={`Bạn có chắc chắn muốn xóa email "${clickEmail?.email}" không?`}
|
||||||
|
opened={opened}
|
||||||
|
onCancel={close}
|
||||||
|
onConfirm={() => handleDeleteEmail(clickEmail?.id)}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MailsPage;
|
export default MailsPage;
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,106 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ActionIcon, Box, ScrollAreaAutosize, Tooltip } from "@mantine/core";
|
import { ActionIcon, Box, LoadingOverlay, ScrollAreaAutosize, Tooltip } from '@mantine/core';
|
||||||
import { useViewportSize } from "@mantine/hooks"; // 🔥 thêm cái này
|
import { useViewportSize } from '@mantine/hooks'; // 🔥 thêm cái này
|
||||||
import { IconDotsVertical, IconMail } from "@tabler/icons-react";
|
import { IconDotsVertical, IconMail } from '@tabler/icons-react';
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Message, Settings } from "../components";
|
import { Message, Settings } from '../components';
|
||||||
import { showNotification } from "../ultils/fn";
|
import { showNotification } from '../ultils/fn';
|
||||||
|
|
||||||
function MainPage() {
|
function MainPage() {
|
||||||
const [messages, setMessages] = useState<IMessage[]>([]);
|
const [messages, setMessages] = useState<IMessage[]>([]);
|
||||||
const viewportRef = useRef<HTMLDivElement>(null);
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const { height } = useViewportSize(); // lấy kích thước cửa sổ
|
const { height } = useViewportSize(); // lấy kích thước cửa sổ
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
const viewport = viewportRef.current;
|
const viewport = viewportRef.current;
|
||||||
if (viewport) {
|
if (viewport) {
|
||||||
viewport.scrollTo({
|
viewport.scrollTo({
|
||||||
top: viewport.scrollHeight,
|
top: viewport.scrollHeight,
|
||||||
behavior: "smooth",
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMessages = async () => {
|
|
||||||
const messages = await window.ipcRenderer.invoke("fetchMessages");
|
|
||||||
setMessages(messages);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const connectSocket = async () => {
|
|
||||||
try {
|
|
||||||
await window.ipcRenderer.invoke("connect-socket");
|
|
||||||
await fetchMessages();
|
|
||||||
setTimeout(scrollToBottom, 100);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to connect socket", error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
connectSocket();
|
const fetchMessages = async () => {
|
||||||
window.ipcRenderer?.onNewNote(async (data: any) => {
|
const messages = await window.ipcRenderer.invoke('fetchMessages');
|
||||||
console.log("Received newNote data:", data);
|
|
||||||
showNotification('Có tin nhắn mới', data.message)
|
|
||||||
await fetchMessages();
|
|
||||||
scrollToBottom()
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openMailsWindow = () => {
|
if (!messages || !Array.isArray(messages)) {
|
||||||
window.ipcRenderer.invoke("open-new-window");
|
showNotification('Lỗi fetch notes', `Error: ${JSON.stringify(messages)}`);
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px)
|
setMessages(messages);
|
||||||
const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
|
};
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Box className="flex flex-col !overflow-hidden h-full">
|
const connectSocket = async () => {
|
||||||
{/* Header */}
|
try {
|
||||||
<header className="p-4 flex items-center justify-between sticky top-0 border-b border-gray-100 pb-2 bg-white z-10">
|
await window.ipcRenderer.invoke('connect-socket');
|
||||||
<Tooltip label="Mail">
|
setLoading(true);
|
||||||
<ActionIcon
|
await fetchMessages();
|
||||||
onClick={openMailsWindow}
|
setTimeout(scrollToBottom, 100);
|
||||||
variant="light"
|
} catch (error) {
|
||||||
radius="md"
|
console.error('Failed to connect socket', error);
|
||||||
size="lg"
|
} finally {
|
||||||
>
|
setLoading(false);
|
||||||
<IconMail size={18} />
|
}
|
||||||
</ActionIcon>
|
};
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div className="text-xl font-semibold">Notes</div>
|
connectSocket();
|
||||||
|
window.ipcRenderer?.onNewNote(async (data: any) => {
|
||||||
|
console.log('Received newNote data:', data);
|
||||||
|
showNotification('Có tin nhắn mới', data.message);
|
||||||
|
await fetchMessages();
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
<Settings>
|
const openMailsWindow = () => {
|
||||||
<Tooltip label="Settings">
|
window.ipcRenderer.invoke('open-new-window');
|
||||||
<ActionIcon variant="subtle" radius="md" size="lg">
|
};
|
||||||
<IconDotsVertical size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Settings>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Message List */}
|
// 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px)
|
||||||
<ScrollAreaAutosize
|
const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
|
||||||
viewportRef={viewportRef}
|
|
||||||
h={scrollAreaHeight} // 👈 set height động theo window size
|
return (
|
||||||
type="auto"
|
<Box className="flex flex-col !overflow-hidden h-full">
|
||||||
>
|
{/* Header */}
|
||||||
<Box className="flex flex-col gap-3 w-full overflow-hidden p-2">
|
<header className="p-4 flex items-center justify-between sticky top-0 border-b border-gray-100 pb-2 bg-white z-10">
|
||||||
{messages.map((item) => (
|
<Tooltip label="Mail">
|
||||||
<Message key={item.message + item.id} data={item} />
|
<ActionIcon onClick={openMailsWindow} variant="light" radius="md" size="lg">
|
||||||
))}
|
<IconMail size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="text-xl font-semibold">Notes</div>
|
||||||
|
|
||||||
|
<Settings>
|
||||||
|
<Tooltip label="Settings">
|
||||||
|
<ActionIcon variant="subtle" radius="md" size="lg">
|
||||||
|
<IconDotsVertical size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Settings>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Message List */}
|
||||||
|
<ScrollAreaAutosize
|
||||||
|
viewportRef={viewportRef}
|
||||||
|
h={scrollAreaHeight} // 👈 set height động theo window size
|
||||||
|
type="auto"
|
||||||
|
>
|
||||||
|
<Box className="flex flex-col gap-3 w-full overflow-hidden p-2">
|
||||||
|
{messages.map((item) => (
|
||||||
|
<Message key={item.message + item.id} data={item} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</ScrollAreaAutosize>
|
||||||
|
|
||||||
|
<LoadingOverlay visible={loading} />
|
||||||
</Box>
|
</Box>
|
||||||
</ScrollAreaAutosize>
|
);
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MainPage;
|
export default MainPage;
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,22 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: {
|
electronAPI: {
|
||||||
// openDevTools: () => void;
|
// openDevTools: () => void;
|
||||||
onNewNote: (data: any) => void;
|
onNewNote: (data: any) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMessage {
|
interface IMessage {
|
||||||
id: number;
|
id: number;
|
||||||
sender: string;
|
sender: string;
|
||||||
time: number;
|
time: number;
|
||||||
message: string;
|
message: string;
|
||||||
animation?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEmail {
|
interface IEmail {
|
||||||
id: number;
|
id: number;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// tailwind.config.js
|
||||||
|
export default {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
blink: {
|
||||||
|
'0%, 100%': { opacity: '1' },
|
||||||
|
'50%': { opacity: '0' },
|
||||||
|
},
|
||||||
|
flash: {
|
||||||
|
'0%, 100%': { opacity: '1' },
|
||||||
|
'25%, 75%': { opacity: '0' },
|
||||||
|
'50%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
pop: {
|
||||||
|
'0%': { transform: 'scale(1)' },
|
||||||
|
'50%': { transform: 'scale(1.05)' },
|
||||||
|
'100%': { transform: 'scale(1)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
blink: 'blink 1s ease-in-out',
|
||||||
|
flash: 'flash 1s ease-in-out',
|
||||||
|
pop: 'pop 0.3s ease-in-out',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue