delete note
This commit is contained in:
parent
77019ec8b3
commit
8b6565a163
|
|
@ -4,7 +4,7 @@ import { app, BrowserWindow, ipcMain, Menu, Notification, screen } from 'electro
|
||||||
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 { addEmail, deleteEmail, deleteMessage, fetchEmails, fetchMessages } from '../src/apis';
|
||||||
import { createMailWindow } from './windows/mails.window';
|
import { createMailWindow } from './windows/mails.window';
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
|
@ -53,8 +53,8 @@ function createWindow() {
|
||||||
// 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: 400,
|
||||||
height: 200,
|
height: 400,
|
||||||
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
|
||||||
|
|
@ -70,7 +70,7 @@ function createWindow() {
|
||||||
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 - 400, height - 400);
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) {
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL);
|
win.loadURL(VITE_DEV_SERVER_URL);
|
||||||
|
|
@ -148,6 +148,10 @@ ipcMain.handle('del-email', async (_, id: number) => {
|
||||||
return deleteEmail(id);
|
return deleteEmail(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('del-message', async (_, id: number) => {
|
||||||
|
return deleteMessage(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,
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,62 @@
|
||||||
import axios from '../instants/axios';
|
import axios from "../instants/axios";
|
||||||
|
|
||||||
const API_KEY = import.meta.env.VITE_API_KEY;
|
const API_KEY = import.meta.env.VITE_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(email: string, onError?: (error: unknown) => void) {
|
export async function addEmail(
|
||||||
try {
|
email: string,
|
||||||
const response = await axios.post(`add-email`, {
|
onError?: (error: unknown) => void
|
||||||
email: email,
|
) {
|
||||||
key: API_KEY,
|
try {
|
||||||
});
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMessage(id: number) {
|
||||||
|
await axios.delete(`delete-note/${id}?key=${API_KEY}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
export interface IMessageProps {
|
export interface IMessageProps {
|
||||||
data: IMessage;
|
data: IMessage;
|
||||||
|
onDelete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message({ data }: IMessageProps) {
|
export default function Message({ data, onDelete }: IMessageProps) {
|
||||||
const [show, { toggle }] = useDisclosure(false);
|
const [show, { toggle }] = useDisclosure(false);
|
||||||
|
|
||||||
const [animation, setAnimation] = useState(false);
|
const [animation, setAnimation] = useState(false);
|
||||||
|
|
@ -27,7 +28,7 @@ export default function Message({ data }: IMessageProps) {
|
||||||
}, [animation]);
|
}, [animation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showAnimation = new Date().getTime() - new Date(data.time).getTime() <= 3000;
|
const showAnimation = new Date().getTime() - new Date(data.time).getTime() <= 110000;
|
||||||
|
|
||||||
setAnimation(showAnimation);
|
setAnimation(showAnimation);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -51,7 +52,7 @@ export default function Message({ data }: IMessageProps) {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Delete">
|
<Tooltip label="Delete">
|
||||||
<ActionIcon variant="light" size="sm" color="red">
|
<ActionIcon onClick={onDelete} variant="light" size="sm" color="red">
|
||||||
<IconTrash size={16} />
|
<IconTrash size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { Menu } from "@mantine/core";
|
import { Menu } from "@mantine/core";
|
||||||
import { IconCode } from "@tabler/icons-react";
|
import { IconCode, IconRefresh } from "@tabler/icons-react";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
export interface ISettingsProps {
|
export interface ISettingsProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
funtions?: {
|
||||||
|
refresh?: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings({ children }: ISettingsProps) {
|
export default function Settings({ children, funtions }: ISettingsProps) {
|
||||||
const openDevtools = () => {
|
const openDevtools = () => {
|
||||||
window.ipcRenderer?.openDevTools();
|
window.ipcRenderer?.openDevTools();
|
||||||
};
|
};
|
||||||
|
|
@ -19,6 +22,9 @@ export default function Settings({ children }: ISettingsProps) {
|
||||||
<Menu.Item onClick={openDevtools} leftSection={<IconCode size={12} />} className="text-xs">
|
<Menu.Item onClick={openDevtools} leftSection={<IconCode size={12} />} className="text-xs">
|
||||||
Devtool
|
Devtool
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item onClick={funtions?.refresh} leftSection={<IconRefresh size={12} />} className="text-xs">
|
||||||
|
Refresh
|
||||||
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,158 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ActionIcon, Box, LoadingOverlay, ScrollAreaAutosize, Tooltip } from '@mantine/core';
|
import {
|
||||||
import { useViewportSize } from '@mantine/hooks'; // 🔥 thêm cái này
|
ActionIcon,
|
||||||
import { IconDotsVertical, IconMail } from '@tabler/icons-react';
|
Box,
|
||||||
import { useEffect, useRef, useState } from 'react';
|
LoadingOverlay,
|
||||||
import { Message, Settings } from '../components';
|
ScrollAreaAutosize,
|
||||||
import { showNotification } from '../ultils/fn';
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure, useViewportSize } from "@mantine/hooks"; // 🔥 thêm cái này
|
||||||
|
import { IconDotsVertical, IconMail } from "@tabler/icons-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { ConfirmModal, Message, Settings } from "../components";
|
||||||
|
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 [loading, setLoading] = useState(false);
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const { height } = useViewportSize(); // lấy kích thước cửa sổ
|
||||||
|
const [clickMessage, setClickMessage] = useState<IMessage | null>(null);
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const viewport = viewportRef.current;
|
||||||
|
if (viewport) {
|
||||||
|
viewport.scrollTo({
|
||||||
|
top: viewport.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { height } = useViewportSize(); // lấy kích thước cửa sổ
|
const fetchMessages = async () => {
|
||||||
|
const messages = await window.ipcRenderer.invoke("fetchMessages");
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
if (!messages || !Array.isArray(messages)) {
|
||||||
const viewport = viewportRef.current;
|
showNotification("Lỗi fetch notes", `Error: ${JSON.stringify(messages)}`);
|
||||||
if (viewport) {
|
return;
|
||||||
viewport.scrollTo({
|
}
|
||||||
top: viewport.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
setMessages(messages);
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
const handleDeleteMessage = async (id?: number) => {
|
||||||
|
try {
|
||||||
|
if (!id) return;
|
||||||
|
setLoading(true);
|
||||||
|
await window.ipcRenderer.invoke("del-message", id);
|
||||||
|
|
||||||
|
await fetchMessages();
|
||||||
|
close();
|
||||||
|
|
||||||
|
showNotification("Thông báo", "Đã xóa note: " + clickMessage?.id);
|
||||||
|
setClickMessage(null)
|
||||||
|
} catch (error) {
|
||||||
|
console.log("%csrc/pages/mails.tsx:88 error", "color: #007acc;", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMessages = async () => {
|
connectSocket();
|
||||||
const messages = await window.ipcRenderer.invoke('fetchMessages');
|
window.ipcRenderer?.onNewNote(async (data: any) => {
|
||||||
|
console.log("Received newNote data:", data);
|
||||||
|
showNotification("Có tin nhắn mới", data.message);
|
||||||
|
await fetchMessages();
|
||||||
|
setTimeout(() => scrollToBottom(), 100);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!messages || !Array.isArray(messages)) {
|
const openMailsWindow = () => {
|
||||||
showNotification('Lỗi fetch notes', `Error: ${JSON.stringify(messages)}`);
|
window.ipcRenderer.invoke("open-new-window");
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
setMessages(messages);
|
// 🧠 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
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
const connectSocket = async () => {
|
<Box className="flex flex-col !overflow-hidden h-full">
|
||||||
try {
|
{/* Header */}
|
||||||
await window.ipcRenderer.invoke('connect-socket');
|
<header className="p-4 flex items-center justify-between sticky top-0 border-b border-gray-100 pb-2 bg-white z-10">
|
||||||
setLoading(true);
|
<Tooltip label="Mail">
|
||||||
await fetchMessages();
|
<ActionIcon
|
||||||
setTimeout(scrollToBottom, 100);
|
onClick={openMailsWindow}
|
||||||
} catch (error) {
|
variant="light"
|
||||||
console.error('Failed to connect socket', error);
|
radius="md"
|
||||||
} finally {
|
size="lg"
|
||||||
setLoading(false);
|
>
|
||||||
}
|
<IconMail size={18} />
|
||||||
};
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
connectSocket();
|
<div className="text-xl font-semibold">Notes</div>
|
||||||
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 = () => {
|
<Settings
|
||||||
window.ipcRenderer.invoke('open-new-window');
|
funtions={{
|
||||||
};
|
refresh: async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await fetchMessages();
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip label="Settings">
|
||||||
|
<ActionIcon variant="subtle" radius="md" size="lg">
|
||||||
|
<IconDotsVertical size={18} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Settings>
|
||||||
|
</header>
|
||||||
|
|
||||||
// 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px)
|
{/* Message List */}
|
||||||
const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
|
<ScrollAreaAutosize
|
||||||
|
viewportRef={viewportRef}
|
||||||
return (
|
h={scrollAreaHeight} // 👈 set height động theo window size
|
||||||
<Box className="flex flex-col !overflow-hidden h-full">
|
type="auto"
|
||||||
{/* Header */}
|
>
|
||||||
<header className="p-4 flex items-center justify-between sticky top-0 border-b border-gray-100 pb-2 bg-white z-10">
|
<Box className="flex flex-col gap-3 w-full overflow-hidden p-2">
|
||||||
<Tooltip label="Mail">
|
{messages.map((item) => (
|
||||||
<ActionIcon onClick={openMailsWindow} variant="light" radius="md" size="lg">
|
<Message
|
||||||
<IconMail size={18} />
|
onDelete={() => {
|
||||||
</ActionIcon>
|
setClickMessage(item);
|
||||||
</Tooltip>
|
open();
|
||||||
|
}}
|
||||||
<div className="text-xl font-semibold">Notes</div>
|
key={item.message + item.id}
|
||||||
|
data={item}
|
||||||
<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>
|
||||||
|
|
||||||
|
<LoadingOverlay visible={loading} />
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
title="Warning"
|
||||||
|
message={`Bạn có chắc chắn muốn xóa note "${clickMessage?.id}" không?`}
|
||||||
|
opened={opened}
|
||||||
|
onCancel={close}
|
||||||
|
onConfirm={() => handleDeleteMessage(clickMessage?.id)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MainPage;
|
export default MainPage;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue