Compare commits
	
		
			2 Commits
		
	
	
		
			42aeeb3e8d
			...
			77019ec8b3
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
								 | 
						77019ec8b3 | |
| 
							
							
								 | 
						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