diff --git a/.env b/.env
index 781d4de..5e7091c 100644
--- a/.env
+++ b/.env
@@ -1 +1 @@
-API_KEY = '@yQveRWre@b(!_9HmL'
\ No newline at end of file
+VITE_API_KEY = '@yQveRWre@b(!_9HmL'
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..194a2de
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+VITE_API_KEY = ''
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f4bb2e4..7d8596e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,7 @@ dist-electron
release
dist-ssr
*.local
+.env
# Editor directories and files
.vscode/*
diff --git a/electron-builder.json5 b/electron-builder.json5
index cd633dc..4ebb93b 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -1,43 +1,34 @@
// @see - https://www.electron.build/configuration/configuration
{
- "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
- "appId": "YourAppID",
- "asar": true,
- "productName": "YourAppName",
- "directories": {
- "output": "release/${version}"
- },
- "files": [
- "dist",
- "dist-electron"
- ],
- "mac": {
- "target": [
- "dmg"
- ],
- "artifactName": "${productName}-Mac-${version}-Installer.${ext}"
- },
- "win": {
- "target": [
- {
- "target": "nsis",
- "arch": [
- "x64"
- ]
- }
- ],
- "artifactName": "${productName}-Windows-${version}-Setup.${ext}"
- },
- "nsis": {
- "oneClick": false,
- "perMachine": false,
- "allowToChangeInstallationDirectory": true,
- "deleteAppDataOnUninstall": false
- },
- "linux": {
- "target": [
- "AppImage"
- ],
- "artifactName": "${productName}-Linux-${version}.${ext}"
- }
+ $schema: 'https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json',
+ appId: 'b1730d2e-6f3c-4f92-9f4a-9f8e918b40d2',
+ asar: true,
+ productName: 'Zulip messages',
+ directories: {
+ output: 'release/${version}',
+ },
+ files: ['dist', 'dist-electron'],
+ mac: {
+ target: ['dmg'],
+ artifactName: '${productName}-Mac-${version}-Installer.${ext}',
+ },
+ win: {
+ target: [
+ {
+ target: 'nsis',
+ arch: ['x64'],
+ },
+ ],
+ artifactName: '${productName}-Windows-${version}-Setup.${ext}',
+ },
+ nsis: {
+ oneClick: false,
+ perMachine: false,
+ allowToChangeInstallationDirectory: true,
+ deleteAppDataOnUninstall: false,
+ },
+ linux: {
+ target: ['AppImage'],
+ artifactName: '${productName}-Linux-${version}.${ext}',
+ },
}
diff --git a/electron/main.ts b/electron/main.ts
index 4256207..5ec1404 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -1,17 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { config } from "dotenv";
-import { app, BrowserWindow, ipcMain, Notification, screen } from "electron";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-import { io } from "socket.io-client";
-import { WebSocket } from "ws";
-import { addEmail, deleteEmail, fetchEmails, fetchMessages } from "../src/apis";
+import { app, BrowserWindow, ipcMain, Menu, Notification, screen } from 'electron';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { io } from 'socket.io-client';
+import { addEmail, deleteEmail, fetchEmails, fetchMessages } from '../src/apis';
+import { createMailWindow } from './windows/mails.window';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
-// dot env
-config();
-
// The built directory structure
//
// ├─┬─┬ dist
@@ -21,174 +17,142 @@ config();
// │ │ ├── main.js
// │ │ └── preload.mjs
// │
-process.env.APP_ROOT = path.join(__dirname, "..");
-
-let newWin: BrowserWindow | null = null;
+process.env.APP_ROOT = path.join(__dirname, '..');
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
-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 RENDERER_DIST = path.join(process.env.APP_ROOT, "dist");
+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 RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
-process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
- ? path.join(process.env.APP_ROOT, "public")
- : RENDERER_DIST;
+process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST;
let win: BrowserWindow | null;
+Menu.setApplicationMenu(null);
+
function createWindow() {
- // Lấy thông tin tất cả các màn hình
- const displays = screen.getAllDisplays();
+ // Lấy thông tin tất cả các màn hình
+ const displays = screen.getAllDisplays();
- // Lấy vị trí con trỏ chuột
- const cursorPoint = screen.getCursorScreenPoint();
+ // Lấy vị trí con trỏ chuột
+ const cursorPoint = screen.getCursorScreenPoint();
- // Tìm màn hình có chứa con trỏ chuột
- let display = displays.find((display) => {
- const { x, y, width, height } = display.bounds;
- return (
- cursorPoint.x >= x &&
- cursorPoint.x <= x + width &&
- cursorPoint.y >= y &&
- cursorPoint.y <= y + height
- );
- });
+ // Tìm màn hình có chứa con trỏ chuột
+ let display = displays.find((display) => {
+ const { x, y, width, height } = display.bounds;
+ return 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
- if (!display) {
- display = screen.getPrimaryDisplay();
- }
+ // 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) {
+ 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({
- width: 600,
- height: 200,
- x: 0, // Đặt cửa sổ ở góc phả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
- resizable: true, // Không cho phép thay đổi kích thước
- icon: path.join(process.env.VITE_PUBLIC, "electron-vite.svg"),
- webPreferences: {
- preload: path.join(__dirname, "preload.mjs"),
- },
- });
+ win = new BrowserWindow({
+ width: 600,
+ height: 200,
+ x: 0, // Đặt cửa sổ ở góc phả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
+ resizable: true, // Không cho phép thay đổi kích thước
+ icon: path.join(process.env.VITE_PUBLIC, 'electron-vite.svg'),
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.mjs'),
+ },
+ });
- // Test active push message to Renderer-process.
- win.webContents.on("did-finish-load", () => {
- win?.webContents.send("main-process-message", new Date().toLocaleString());
- });
+ // Test active push message to Renderer-process.
+ win.webContents.on('did-finish-load', () => {
+ 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) {
- win.loadURL(VITE_DEV_SERVER_URL);
- } else {
- // win.loadFile('dist/index.html')
- win.loadFile(path.join(RENDERER_DIST, "index.html"));
- }
+ if (VITE_DEV_SERVER_URL) {
+ win.loadURL(VITE_DEV_SERVER_URL);
+ } else {
+ // win.loadFile('dist/index.html')
+ win.loadFile(path.join(RENDERER_DIST, 'index.html'));
+ }
}
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
-app.on("window-all-closed", () => {
- if (process.platform !== "darwin") {
- app.quit();
- win = null;
- }
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ win = null;
+ }
});
-app.on("activate", () => {
- // 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.
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- }
+app.on('activate', () => {
+ // 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.
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
});
app.whenReady().then(createWindow);
// IPC Main Events
-ipcMain.on("open-devtools", (event) => {
- const webContents = event.sender;
- webContents.openDevTools({ mode: "detach" });
+ipcMain.on('open-devtools', (event) => {
+ const webContents = event.sender;
+ webContents.openDevTools({ mode: 'detach' });
});
// Xử lý connect socket
-ipcMain.handle("connect-socket", async (_) => {
- const socket = io("https://zulip.ipsupply.com.au", {
- path: "/apac-custom/socket.io",
- secure: true,
- query: { token: process.env.API_KEY },
- rejectUnauthorized: false,
- });
+ipcMain.handle('connect-socket', async (_) => {
+ const socket = io('https://zulip.ipsupply.com.au', {
+ path: '/apac-custom/socket.io',
+ secure: true,
+ query: { token: import.meta.env.VITE_API_KEY },
+ rejectUnauthorized: false,
+ });
- if (!socket.connected) {
- socket.connect();
- }
+ if (!socket.connected) {
+ socket.connect();
+ }
- socket.on("connect", () => {
- console.log(socket.connected); // true
- });
+ socket.on('connect', () => {
+ console.log(socket.connected); // true
+ });
- socket.on("newNote", (data) => {
- win?.webContents.send("newNote", data);
- });
+ socket.on('newNote', (data) => {
+ win?.webContents.send('newNote', data);
+ });
});
-ipcMain.handle("fetchMessages", async () => {
- return await fetchMessages();
+ipcMain.handle('fetchMessages', async () => {
+ return await fetchMessages();
});
-ipcMain.handle("fetchEmails", async () => {
- return await fetchEmails();
+ipcMain.handle('fetchEmails', async () => {
+ return await fetchEmails();
});
-ipcMain.handle("open-new-window", async () => {
- createMailWindow();
+ipcMain.handle('open-new-window', async () => {
+ createMailWindow({ RENDERER_DIST });
});
-ipcMain.handle("add-email", async (_, email: string) => {
- return addEmail(email);
+ipcMain.handle('add-email', async (_, email: string) => {
+ return addEmail(email);
});
-ipcMain.handle("del-email", async (_, id: number) => {
- return deleteEmail(id);
+ipcMain.handle('del-email', async (_, id: number) => {
+ return deleteEmail(id);
});
-ipcMain.handle("show-notification", async (_, { title, body }) => {
- const notification = new Notification({
- title,
- body,
- silent: false,
- });
- notification.show();
+ipcMain.handle('show-notification', async (_, { title, body }) => {
+ const notification = new Notification({
+ title,
+ body,
+ silent: false,
+ });
+ 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;
- });
-}
diff --git a/electron/windows/mails.window.ts b/electron/windows/mails.window.ts
new file mode 100644
index 0000000..ffa70e8
--- /dev/null
+++ b/electron/windows/mails.window.ts
@@ -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;
+}
diff --git a/package-lock.json b/package-lock.json
index 3f909f2..2f73f97 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,14 +16,12 @@
"@types/ws": "^8.18.1",
"axios": "^1.9.0",
"bufferutil": "^4.0.9",
- "dotenv": "^16.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.5.3",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.4",
"utf-8-validate": "^6.0.5",
- "ws": "^8.18.2",
"zod": "^3.24.4"
},
"devDependencies": {
@@ -3916,18 +3914,6 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz",
@@ -8228,27 +8214,6 @@
"dev": true,
"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": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
diff --git a/package.json b/package.json
index 73bb37c..02cdd57 100644
--- a/package.json
+++ b/package.json
@@ -19,14 +19,12 @@
"@types/ws": "^8.18.1",
"axios": "^1.9.0",
"bufferutil": "^4.0.9",
- "dotenv": "^16.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.5.3",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.4",
"utf-8-validate": "^6.0.5",
- "ws": "^8.18.2",
"zod": "^3.24.4"
},
"devDependencies": {
diff --git a/src/apis/index.ts b/src/apis/index.ts
index 91446af..e33c169 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -1,61 +1,55 @@
-import { config } from "dotenv";
-import axios from "../instants/axios";
+import axios from '../instants/axios';
-config();
-
-const API_KEY = process.env.API_KEY;
+const API_KEY = import.meta.env.VITE_API_KEY;
export const getAllNote = async () => {
- try {
- const response = await axios({
- method: "GET",
- url: "getAllNotes",
- });
+ try {
+ const response = await axios({
+ method: 'GET',
+ url: 'getAllNotes',
+ });
- return response.data;
- } catch (error) {
- return [];
- }
+ return response.data;
+ } catch (error) {
+ return [];
+ }
};
export async function fetchMessages(onError?: (error: unknown) => void) {
- try {
- const response = await axios.get(`getAllNotes?key=${API_KEY}`);
+ try {
+ const response = await axios.get(`getAllNotes?key=${API_KEY}`);
- return response.data;
- } catch (error) {
- console.error("Error fetching notes:", error);
- onError?.(error);
- }
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching notes:', error);
+ onError?.(error);
+ }
}
export async function fetchEmails(onError?: (error: unknown) => void) {
- try {
- const response = await axios.get(`emails?key=${API_KEY}`);
- return response.data;
- } catch (error) {
- console.error("Error fetching emails:", error);
- onError?.(error);
- }
+ try {
+ const response = await axios.get(`emails?key=${API_KEY}`);
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching emails:', error);
+ onError?.(error);
+ }
}
-export async function addEmail(
- email: string,
- onError?: (error: unknown) => void
-) {
- try {
- const response = await axios.post(`add-email`, {
- email: email,
- key: API_KEY,
- });
+export async function addEmail(email: string, onError?: (error: unknown) => void) {
+ try {
+ const response = await axios.post(`add-email`, {
+ email: email,
+ key: API_KEY,
+ });
- return response.data;
- } catch (error) {
- console.error("Error add email:", error);
- onError?.(error);
- }
+ return response.data;
+ } catch (error) {
+ console.error('Error add email:', error);
+ onError?.(error);
+ }
}
export async function deleteEmail(id: number) {
- await axios.delete(`delete-email/${id}?key=${API_KEY}`);
+ await axios.delete(`delete-email/${id}?key=${API_KEY}`);
}
diff --git a/src/components/confirm-modal.tsx b/src/components/confirm-modal.tsx
index 2ba949c..7b703a7 100644
--- a/src/components/confirm-modal.tsx
+++ b/src/components/confirm-modal.tsx
@@ -1,34 +1,28 @@
-import { Button, Modal, Text } from "@mantine/core";
+import { Button, Modal, Text } from '@mantine/core';
interface ConfirmModalProps {
- opened: boolean;
- title?: string;
- message: string;
- onConfirm: () => void;
- onCancel: () => void;
+ opened: boolean;
+ title?: string;
+ message: string;
+ onConfirm: () => void;
+ onCancel: () => void;
}
-export default function ConfirmModal({
- opened,
- title = "Xác nhận",
- message,
- onConfirm,
- onCancel,
-}: ConfirmModalProps) {
- return (
-
-
- {message}
-
+export default function ConfirmModal({ opened, title = 'Xác nhận', message, onConfirm, onCancel }: ConfirmModalProps) {
+ return (
+
+
+ {message}
+
-
-
-
-
-
- );
+
+
+
+
+
+ );
}
diff --git a/src/components/message.tsx b/src/components/message.tsx
index 1ce8af7..d2c5771 100644
--- a/src/components/message.tsx
+++ b/src/components/message.tsx
@@ -1,48 +1,62 @@
-import { ActionIcon, Box, Paper, Textarea, Tooltip } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
-import { IconEye, IconEyeOff, IconTrash } from "@tabler/icons-react";
-import { useMemo } from "react";
+import { ActionIcon, Box, Paper, Textarea, Tooltip } from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
+import { IconEye, IconEyeOff, IconTrash } from '@tabler/icons-react';
+import { useEffect, useMemo, useState } from 'react';
export interface IMessageProps {
- data: IMessage;
+ data: IMessage;
}
export default function Message({ data }: IMessageProps) {
- const [show, { toggle }] = useDisclosure(false);
+ const [show, { toggle }] = useDisclosure(false);
- const messageContent = useMemo(() => {
- return show ? data.message : data.message.replace(/./g, "•");
- }, [data.message, show]);
+ const [animation, setAnimation] = useState(false);
- return (
-
-
-
-
-
-
- {show ? : }
-
-
-
-
-
-
-
-
-
-
- );
+ const messageContent = useMemo(() => {
+ return show ? data.message : data.message.replace(/./g, '•');
+ }, [data.message, show]);
+
+ // Tắt hiệu ứng nhấp nháy sau 1 giây
+ useEffect(() => {
+ if (!animation) return;
+
+ const timer = setTimeout(() => {
+ setAnimation(false);
+ }, 5000);
+ return () => clearTimeout(timer);
+ }, [animation]);
+
+ useEffect(() => {
+ const showAnimation = new Date().getTime() - new Date(data.time).getTime() <= 3000;
+
+ setAnimation(showAnimation);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+
+
+
+
+ {show ? : }
+
+
+
+
+
+
+
+
+
+
+ );
}
diff --git a/src/pages/mails.tsx b/src/pages/mails.tsx
index 1162af9..c79b1da 100644
--- a/src/pages/mails.tsx
+++ b/src/pages/mails.tsx
@@ -1,178 +1,160 @@
-import {
- ActionIcon,
- Box,
- Button,
- Container,
- LoadingOverlay,
- Paper,
- ScrollArea,
- Text,
- 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";
+import { ActionIcon, Box, Button, Container, LoadingOverlay, Paper, ScrollArea, Text, 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({
- email: z.string().email("Invalid email"),
+ email: z.string().email('Invalid email'),
});
function MailsPage() {
- const viewport = useRef(null);
- const [opened, { open, close }] = useDisclosure(false);
- const [clickEmail, setClickEmail] = useState(null);
- const [emails, setEmails] = useState([]); // Dùng để lưu danh sách email
+ const viewport = useRef(null);
+ const [opened, { open, close }] = useDisclosure(false);
+ const [clickEmail, setClickEmail] = useState(null);
+ const [emails, setEmails] = useState([]); // Dùng để lưu danh sách email
- const [loading, setLoading] = useState(false);
- const form = useForm({
- initialValues: {
- email: "",
- },
- validate: zodResolver(schema),
- });
-
- const scrollToBottom = () =>
- viewport.current!.scrollTo({
- top: viewport.current!.scrollHeight,
- behavior: "smooth",
+ const [loading, setLoading] = useState(false);
+ const form = useForm({
+ initialValues: {
+ email: '',
+ },
+ validate: zodResolver(schema),
});
- // 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);
-
- if (!response) return;
-
- console.log("%csrc/pages/mails.tsx:40 {response}", "color: #007acc;", {
- response,
+ const scrollToBottom = () =>
+ viewport.current!.scrollTo({
+ top: viewport.current!.scrollHeight,
+ behavior: 'smooth',
});
- 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();
- setClickEmail(null);
- form.reset();
- } catch (error) {
- console.log("%csrc/pages/mails.tsx:65 error", "color: #007acc;", error);
- } finally {
- setLoading(false);
- }
- }
- };
+ console.log('%csrc/pages/mails.tsx:40 {response}', 'color: #007acc;', {
+ response,
+ });
- const handleDeleteEmail = async (id: number | undefined) => {
- try {
- if (!id) return;
- setLoading(true);
- await window.ipcRenderer.invoke("del-email", id);
+ await fetchEmails();
- await fetchEmails();
- close();
+ showNotification('Thành công', 'Đã thêm email mới');
- showNotification("Thông báo", "Đã xóa email");
- } catch (error) {
- console.log("%csrc/pages/mails.tsx:88 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);
+ scrollToBottom();
+ setClickEmail(null);
+ form.reset();
+ } catch (error) {
+ console.log('%csrc/pages/mails.tsx:65 error', 'color: #007acc;', error);
+ } finally {
+ setLoading(false);
+ }
+ }
};
- }, []);
- return (
-
-
-
-
+ const handleDeleteEmail = async (id: number | undefined) => {
+ try {
+ if (!id) return;
+ setLoading(true);
+ await window.ipcRenderer.invoke('del-email', id);
-
-
- {emails.length > 0 ? (
- emails.map((item) => (
-
-
-
-
- {item.email}
- {
- setClickEmail(item);
- open();
- }}
- color="red"
- size="md"
- variant="light"
- >
-
-
-
- ))
- ) : (
- No emails added yet.
- )}
-
-
+ await fetchEmails();
+ close();
-
+ showNotification('Thông báo', 'Đã xóa email');
+ } catch (error) {
+ console.log('%csrc/pages/mails.tsx:88 error', 'color: #007acc;', error);
+ } finally {
+ setLoading(false);
+ }
+ };
- handleDeleteEmail(clickEmail?.id)}
- />
-
- );
+ 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 (
+
+
+
+
+
+
+
+ {emails.length > 0 ? (
+ emails.map((item) => (
+
+
+
+
+ {item.email}
+ {
+ setClickEmail(item);
+ open();
+ }}
+ color="red"
+ size="md"
+ variant="light"
+ >
+
+
+
+ ))
+ ) : (
+ No emails added yet.
+ )}
+
+
+
+
+
+ handleDeleteEmail(clickEmail?.id)}
+ />
+
+ );
}
export default MailsPage;
diff --git a/src/pages/main.tsx b/src/pages/main.tsx
index 0897e78..df14669 100644
--- a/src/pages/main.tsx
+++ b/src/pages/main.tsx
@@ -1,99 +1,106 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-import { ActionIcon, Box, ScrollAreaAutosize, Tooltip } from "@mantine/core";
-import { useViewportSize } from "@mantine/hooks"; // 🔥 thêm cái này
-import { IconDotsVertical, IconMail } from "@tabler/icons-react";
-import { useEffect, useRef, useState } from "react";
-import { Message, Settings } from "../components";
-import { showNotification } from "../ultils/fn";
+import { ActionIcon, Box, LoadingOverlay, ScrollAreaAutosize, Tooltip } from '@mantine/core';
+import { useViewportSize } from '@mantine/hooks'; // 🔥 thêm cái này
+import { IconDotsVertical, IconMail } from '@tabler/icons-react';
+import { useEffect, useRef, useState } from 'react';
+import { Message, Settings } from '../components';
+import { showNotification } from '../ultils/fn';
function MainPage() {
- const [messages, setMessages] = useState([]);
- const viewportRef = useRef(null);
+ const [messages, setMessages] = useState([]);
+ const viewportRef = useRef(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 viewport = viewportRef.current;
- if (viewport) {
- viewport.scrollTo({
- top: viewport.scrollHeight,
- 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);
- }
+ const scrollToBottom = () => {
+ const viewport = viewportRef.current;
+ if (viewport) {
+ viewport.scrollTo({
+ top: viewport.scrollHeight,
+ behavior: 'smooth',
+ });
+ }
};
- 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()
- });
- }, []);
+ const fetchMessages = async () => {
+ const messages = await window.ipcRenderer.invoke('fetchMessages');
- const openMailsWindow = () => {
- window.ipcRenderer.invoke("open-new-window");
- };
+ if (!messages || !Array.isArray(messages)) {
+ showNotification('Lỗi fetch notes', `Error: ${JSON.stringify(messages)}`);
+ return;
+ }
- // 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px)
- const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
+ setMessages(messages);
+ };
- return (
-
- {/* Header */}
-
-
-
-
-
-
+ useEffect(() => {
+ const connectSocket = async () => {
+ try {
+ await window.ipcRenderer.invoke('connect-socket');
+ setLoading(true);
+ await fetchMessages();
+ setTimeout(scrollToBottom, 100);
+ } catch (error) {
+ console.error('Failed to connect socket', error);
+ } finally {
+ setLoading(false);
+ }
+ };
- Notes
+ 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();
+ });
+ }, []);
-
-
-
-
-
-
-
-
+ const openMailsWindow = () => {
+ window.ipcRenderer.invoke('open-new-window');
+ };
- {/* Message List */}
-
-
- {messages.map((item) => (
-
- ))}
+ // 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px)
+ const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+ Notes
+
+
+
+
+
+
+
+
+
+
+ {/* Message List */}
+
+
+ {messages.map((item) => (
+
+ ))}
+
+
+
+
-
-
- );
+ );
}
export default MainPage;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 3173515..f546a3a 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -2,23 +2,22 @@
///
declare global {
- interface Window {
- electronAPI: {
- // openDevTools: () => void;
- onNewNote: (data: any) => void;
- };
- }
+ interface Window {
+ electronAPI: {
+ // openDevTools: () => void;
+ onNewNote: (data: any) => void;
+ };
+ }
}
interface IMessage {
- id: number;
- sender: string;
- time: number;
- message: string;
- animation?: boolean;
+ id: number;
+ sender: string;
+ time: number;
+ message: string;
}
interface IEmail {
- id: number;
- email: string;
+ id: number;
+ email: string;
}
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..701ce8b
--- /dev/null
+++ b/tailwind.config.js
@@ -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',
+ },
+ },
+ },
+};