renove mails

This commit is contained in:
Admin 2025-05-09 08:08:38 +07:00
parent 960f889e92
commit 05f961da62
11 changed files with 150 additions and 102 deletions

View File

@ -1 +1 @@
VITE_API_KEY = '' VITE_API_KEY = '@yQveRWre@b(!_9HmL'

View File

@ -1,30 +1,3 @@
# React + TypeScript + Vite DELETE https://zulip.ipsupply.com.au/apac-custom/api/truncate-notes?key={key}
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. GET https://zulip.ipsupply.com.au/apac-custom/api/getAllNotes?key={key}
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View File

@ -119,7 +119,9 @@ function createWindow() {
} }
function createTray() { function createTray() {
tray = new Tray(path.join(process.env.VITE_PUBLIC, "assets", "icon16x16.png")); tray = new Tray(
path.join(process.env.VITE_PUBLIC, "assets", "icon16x16.png")
);
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ {
@ -228,5 +230,13 @@ ipcMain.handle("show-notification", async (_, { title, body }) => {
body, body,
silent: false, silent: false,
}); });
notification.on("click", () => {
if (win && !isQuiting) {
win.show();
win.focus();
}
});
notification.show(); notification.show();
}); });

BIN
public/assets/icon-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Route, HashRouter as Router, Routes } from "react-router-dom"; import { Route, HashRouter as Router, Routes } from "react-router-dom";
import { MailsPage, MainPage } from "./pages"; import { MainPage } from "./pages";
function App() { function App() {
return ( return (
<Router> <Router>
<Routes> <Routes>
<Route path="/" element={<MainPage />} /> <Route path="/" element={<MainPage />} />
<Route path="/mails" element={<MailsPage />} /> {/* <Route path="/mails" element={<MailsPage />} /> */}
</Routes> </Routes>
</Router> </Router>
); );

View File

@ -1,67 +1,100 @@
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 { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from "react";
import dayjs from 'dayjs' import dayjs from "dayjs";
export interface IMessageProps { export interface IMessageProps {
data: IMessage; data: IMessage;
onDelete: () => void onDelete: () => void;
} }
export default function Message({ data, onDelete }: IMessageProps) { export default function Message({ data, onDelete }: IMessageProps) {
const [show, { toggle }] = useDisclosure(true); const [show, { toggle }] = useDisclosure(true);
const [animation, setAnimation] = useState(false); const [animation, setAnimation] = useState(false);
const messageContent = useMemo(() => { const messageContent = useMemo(() => {
return show ? data.message : data.message.replace(/./g, '•'); return show ? data.message : data.message.replace(/./g, "•");
}, [data.message, show]); }, [data.message, show]);
// Tắt hiệu ứng nhấp nháy sau 1 giây // Tắt hiệu ứng nhấp nháy sau 1 giây
useEffect(() => { useEffect(() => {
if (!animation) return; if (!animation) return;
const timer = setTimeout(() => { const timer = setTimeout(() => {
setAnimation(false); setAnimation(false);
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [animation]); }, [animation]);
useEffect(() => { useEffect(() => {
const showAnimation = new Date().getTime() - new Date(data.time).getTime() <= 110000; 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
}, []); }, []);
return ( return (
<Paper <Tooltip
key={data.message} position="top-start"
shadow="xs" style={{
radius="md" fontSize: "11px",
p="xs" }}
withBorder label={`Sender: ${data.sender}`}
className={`flex items-center relative gap-3 w-full transition-all duration-300 ${animation ? 'animate-pulse !bg-blue-100' : ''}`} >
> <Paper
<div className="flex items-center gap-2 w-full"> key={data.message}
<Textarea defaultValue={messageContent} value={messageContent} className="flex-1" variant="unstyled" readOnly /> shadow="xs"
<Box className="flex gap-1"> radius="md"
<Tooltip label="View"> p="xs"
<ActionIcon onClick={toggle} variant="light" size="sm"> withBorder
{show ? <IconEyeOff size={16} /> : <IconEye size={16} />} className={`flex items-center relative gap-3 w-full transition-all duration-300 ${
</ActionIcon> animation ? "animate-pulse !bg-blue-100" : ""
</Tooltip> }`}
<Tooltip label="Delete"> >
<ActionIcon onClick={onDelete} variant="light" size="sm" color="red"> <div className="flex items-center w-full flex-col">
<IconTrash size={16} /> <Box className="flex items-center justify-between w-full">
</ActionIcon> <div className="text-xs">
</Tooltip> {data.recipient.length > 0 && <span>{data.recipient}</span>}
</Box>
</div> </div>
<div>
<span className="text-xs">
{dayjs(data.time).format("HH:mm D/M/YYYY")}
</span>
</div>
</Box>
<span className='absolute top-1 right-2 text-xs'>{dayjs(data.time).format('HH:ss:mm DD/MM/YYYY')}</span> <Box className="w-full flex items-center">
</Paper> <Textarea
); defaultValue={messageContent}
value={messageContent}
className="flex-1"
variant="unstyled"
readOnly
/>
<Box className="flex gap-2">
<Tooltip label="View">
<ActionIcon onClick={toggle} variant="light" size="sm">
{show ? <IconEyeOff size={16} /> : <IconEye size={16} />}
</ActionIcon>
</Tooltip>
<Tooltip label="Delete">
<ActionIcon
onClick={onDelete}
variant="light"
size="sm"
color="red"
>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Box>
</Box>
</div>
</Paper>
</Tooltip>
);
} }

View File

@ -1,11 +1,11 @@
import { Menu } from "@mantine/core"; import { Menu, useMantineColorScheme } from "@mantine/core";
import { IconCode, IconRefresh } from "@tabler/icons-react"; import { IconCode, IconMoon, IconRefresh, IconSun } from "@tabler/icons-react";
import { ReactNode } from "react"; import { ReactNode } from "react";
export interface ISettingsProps { export interface ISettingsProps {
children: ReactNode; children: ReactNode;
funtions?: { funtions?: {
refresh?: () => void refresh?: () => void;
} };
} }
export default function Settings({ children, funtions }: ISettingsProps) { export default function Settings({ children, funtions }: ISettingsProps) {
@ -13,18 +13,34 @@ export default function Settings({ children, funtions }: ISettingsProps) {
window.ipcRenderer?.openDevTools(); window.ipcRenderer?.openDevTools();
}; };
const {toggleColorScheme, colorScheme}= useMantineColorScheme()
return ( return (
<Menu shadow="md" width={200}> <Menu shadow="md" width={200}>
<Menu.Target>{children}</Menu.Target> <Menu.Target>{children}</Menu.Target>
<Menu.Dropdown className="text-xs"> <Menu.Dropdown className="text-xs">
<Menu.Label className="text-xs">Application</Menu.Label> <Menu.Label className="text-xs">Application</Menu.Label>
<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"> <Menu.Item
onClick={funtions?.refresh}
leftSection={<IconRefresh size={12} />}
className="text-xs"
>
Refresh Refresh
</Menu.Item> </Menu.Item>
<Menu.Item
onClick={toggleColorScheme}
leftSection={colorScheme === 'dark'?<IconSun size={12} />: <IconMoon size={12}/>}
className="text-xs flex items-center capitalize"
>
{colorScheme === 'dark' ? 'Light' : 'Dark'}
</Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
); );

View File

@ -5,7 +5,7 @@ import '@mantine/core/styles.css';
import "./index.css"; import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<MantineProvider > <MantineProvider defaultColorScheme="dark">
<App /> <App />
</MantineProvider> </MantineProvider>
); );

View File

@ -1,13 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { import {
ActionIcon, ActionIcon,
Avatar,
Box, Box,
LoadingOverlay, LoadingOverlay,
ScrollAreaAutosize, ScrollAreaAutosize,
Tooltip, Tooltip,
useMantineColorScheme
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useViewportSize } from "@mantine/hooks"; // 🔥 thêm cái này import { useDisclosure, useViewportSize } from "@mantine/hooks"; // 🔥 thêm cái này
import { IconDotsVertical, IconMail } from "@tabler/icons-react"; import { IconDotsVertical } from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { ConfirmModal, Message, Settings } from "../components"; import { ConfirmModal, Message, Settings } from "../components";
import { showNotification } from "../ultils/fn"; import { showNotification } from "../ultils/fn";
@ -19,6 +21,9 @@ function MainPage() {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(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 [clickMessage, setClickMessage] = useState<IMessage | null>(null); const [clickMessage, setClickMessage] = useState<IMessage | null>(null);
const {colorScheme} = useMantineColorScheme();
const scrollToBottom = () => { const scrollToBottom = () => {
const viewport = viewportRef.current; const viewport = viewportRef.current;
if (viewport) { if (viewport) {
@ -50,7 +55,7 @@ function MainPage() {
close(); close();
showNotification("Thông báo", "Đã xóa note: " + clickMessage?.id); showNotification("Thông báo", "Đã xóa note: " + clickMessage?.id);
setClickMessage(null) setClickMessage(null);
} catch (error) { } catch (error) {
console.log("%csrc/pages/mails.tsx:88 error", "color: #007acc;", error); console.log("%csrc/pages/mails.tsx:88 error", "color: #007acc;", error);
} finally { } finally {
@ -81,9 +86,9 @@ function MainPage() {
}); });
}, []); }, []);
const openMailsWindow = () => { // const openMailsWindow = () => {
window.ipcRenderer.invoke("open-new-window"); // window.ipcRenderer.invoke("open-new-window");
}; // };
// 🧠 Tính chiều cao dynamic (ví dụ trừ header 80px + padding 30px) // 🧠 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 const scrollAreaHeight = height - 70; // Bạn chỉnh số này nếu muốn
@ -91,19 +96,24 @@ function MainPage() {
return ( return (
<Box className="flex flex-col !overflow-hidden h-full"> <Box className="flex flex-col !overflow-hidden h-full">
{/* Header */} {/* Header */}
<header className="p-4 flex items-center justify-between sticky top-0 border-b border-gray-100 pb-2 bg-white z-10"> <header className={`px-4 py-2 flex items-center justify-between sticky top-0 border-b z-10 ${colorScheme === 'dark' ? '': 'border-gray-100 pb-2 bg-white'}`}>
<Tooltip label="Mail"> {/* <Tooltip label="Mail">
<ActionIcon <ActionIcon
onClick={openMailsWindow} onClick={() => {
showNotification('test', 'test', scrollToBottom)
}}
variant="light" variant="light"
radius="md" radius="md"
size="lg" size="lg"
> >
<IconMail size={18} /> <IconMail size={18} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip> */}
<div className="text-xl font-semibold">Zulip Notes</div> <Avatar radius="sm" src={"./assets/icon-1.png"} />
{/*
<div className="text-xl font-semibold">Zulip Notes</div> */}
<Settings <Settings
funtions={{ funtions={{

View File

@ -1,6 +1,11 @@
export async function showNotification(title: string, body: string) { export async function showNotification(
title: string,
body: string,
callback?: () => void
) {
await window.ipcRenderer.invoke("show-notification", { await window.ipcRenderer.invoke("show-notification", {
title, title,
body, body,
}); });
callback?.();
} }

1
src/vite-env.d.ts vendored
View File

@ -15,6 +15,7 @@ interface IMessage {
sender: string; sender: string;
time: number; time: number;
message: string; message: string;
recipient:string
} }
interface IEmail { interface IEmail {