Skip to content

Commit 6114625

Browse files
committed
feat: Implement initial application structure with core UI components, project and shortcut management, and Electron integration.
1 parent 190492b commit 6114625

File tree

26 files changed

+1020
-562
lines changed

26 files changed

+1020
-562
lines changed

build_log.txt

3.78 KB
Binary file not shown.

electron/main/index.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow, shell, ipcMain } from 'electron'
1+
import { app, BrowserWindow, shell, ipcMain, dialog } from 'electron'
22
import { createRequire } from 'node:module'
33
import { fileURLToPath } from 'node:url'
44
import path from 'node:path'
@@ -50,11 +50,11 @@ async function createWindow() {
5050
webPreferences: {
5151
preload,
5252
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
53-
// nodeIntegration: true,
53+
nodeIntegration: true,
5454

5555
// Consider using contextBridge.exposeInMainWorld
5656
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
57-
// contextIsolation: false,
57+
contextIsolation: false,
5858
},
5959
})
6060

@@ -121,3 +121,54 @@ ipcMain.handle('open-win', (_, arg) => {
121121
childWindow.loadFile(indexHtml, { hash: arg })
122122
}
123123
})
124+
125+
ipcMain.handle('get-file-icon', async (_, filePath) => {
126+
try {
127+
const icon = await app.getFileIcon(filePath, { size: 'large' });
128+
return icon.toDataURL();
129+
} catch (e) {
130+
console.error('Failed to get icon', e);
131+
return null;
132+
}
133+
});
134+
135+
ipcMain.handle('open-path', async (_, filePath) => {
136+
await shell.openPath(filePath);
137+
});
138+
139+
ipcMain.handle('open-external', async (_, url) => {
140+
await shell.openExternal(url);
141+
});
142+
143+
ipcMain.handle('save-markdown', async (_, { content, defaultPath }) => {
144+
const { canceled, filePath } = await dialog.showSaveDialog({
145+
defaultPath,
146+
filters: [{ name: 'Markdown', extensions: ['md'] }]
147+
});
148+
if (!canceled && filePath) {
149+
const fs = await import('node:fs/promises');
150+
await fs.writeFile(filePath, content);
151+
return true;
152+
}
153+
return false;
154+
});
155+
156+
ipcMain.handle('select-file', async () => {
157+
const { canceled, filePaths } = await dialog.showOpenDialog({
158+
properties: ['openFile']
159+
});
160+
if (!canceled && filePaths.length > 0) {
161+
return filePaths[0];
162+
}
163+
return null;
164+
});
165+
166+
ipcMain.handle('select-folder', async () => {
167+
const { canceled, filePaths } = await dialog.showOpenDialog({
168+
properties: ['openDirectory']
169+
});
170+
if (!canceled && filePaths.length > 0) {
171+
return filePaths[0];
172+
}
173+
return null;
174+
});

electron/preload/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ipcRenderer, contextBridge } from 'electron'
22

33
// --------- Expose some API to the Renderer process ---------
4-
contextBridge.exposeInMainWorld('ipcRenderer', {
4+
const ipcRendererApi = {
55
on(...args: Parameters<typeof ipcRenderer.on>) {
66
const [channel, listener] = args
77
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
@@ -18,10 +18,18 @@ contextBridge.exposeInMainWorld('ipcRenderer', {
1818
const [channel, ...omit] = args
1919
return ipcRenderer.invoke(channel, ...omit)
2020
},
21+
}
2122

22-
// You can expose other APTs you need here.
23-
// ...
24-
})
23+
if (process.contextIsolated) {
24+
try {
25+
contextBridge.exposeInMainWorld('ipcRenderer', ipcRendererApi)
26+
} catch (error) {
27+
console.error(error)
28+
}
29+
} else {
30+
// @ts-ignore
31+
window.ipcRenderer = ipcRendererApi
32+
}
2533

2634
// --------- Preload scripts loading ---------
2735
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@
2020
"test": "vitest run"
2121
},
2222
"dependencies": {
23-
"electron-updater": "^6.3.9"
23+
"@dnd-kit/core": "^6.3.1",
24+
"@dnd-kit/sortable": "^10.0.0",
25+
"@dnd-kit/utilities": "^3.2.2",
26+
"clsx": "^2.1.1",
27+
"electron-store": "^11.0.2",
28+
"electron-updater": "^6.3.9",
29+
"framer-motion": "^12.23.24",
30+
"lucide-react": "^0.554.0",
31+
"react-markdown": "^10.1.0",
32+
"react-textarea-autosize": "^8.5.9",
33+
"tailwind-merge": "^3.4.0",
34+
"uuid": "^13.0.0"
2435
},
2536
"devDependencies": {
2637
"@playwright/test": "^1.48.2",

src/App.tsx

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
1-
import { useState } from 'react'
2-
import UpdateElectron from '@/components/update'
3-
import logoVite from './assets/logo-vite.svg'
4-
import logoElectron from './assets/logo-electron.svg'
5-
import './App.css'
1+
import React from 'react';
2+
import Layout from './components/Layout';
3+
4+
import ShortcutGrid from './components/ShortcutGrid';
5+
import NotesArea from './components/NotesArea';
6+
7+
import CalendarPanel from './components/CalendarPanel';
68

79
function App() {
8-
const [count, setCount] = useState(0)
910
return (
10-
<div className='App'>
11-
<div className='logo-box'>
12-
<a href='https://github.com/electron-vite/electron-vite-react' target='_blank'>
13-
<img src={logoVite} className='logo vite' alt='Electron + Vite logo' />
14-
<img src={logoElectron} className='logo electron' alt='Electron + Vite logo' />
15-
</a>
16-
</div>
17-
<h1>Electron + Vite + React</h1>
18-
<div className='card'>
19-
<button onClick={() => setCount((count) => count + 1)}>
20-
count is {count}
21-
</button>
22-
<p>
23-
Edit <code>src/App.tsx</code> and save to test HMR
24-
</p>
25-
</div>
26-
<p className='read-the-docs'>
27-
Click on the Electron + Vite logo to learn more
28-
</p>
29-
<div className='flex-center'>
30-
Place static files into the<code>/public</code> folder <img style={{ width: '5em' }} src='./node.svg' alt='Node logo' />
31-
</div>
32-
33-
<UpdateElectron />
34-
</div>
35-
)
11+
<Layout
12+
rightPanel={
13+
<div className="flex flex-col h-full">
14+
<div className="h-1/2 border-b border-gray-700">
15+
<CalendarPanel />
16+
</div>
17+
<div className="h-1/2">
18+
<NotesArea />
19+
</div>
20+
</div>
21+
}
22+
>
23+
<ShortcutGrid />
24+
</Layout>
25+
);
3626
}
3727

38-
export default App
28+
export default App;

src/components/CalendarPanel.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
3+
const CalendarPanel: React.FC = () => {
4+
return (
5+
<div className="h-full flex flex-col bg-gray-900 border-l border-gray-700">
6+
<div className="p-4 border-b border-gray-700 flex justify-between items-center">
7+
<h2 className="text-lg font-semibold text-gray-200">Calendar</h2>
8+
</div>
9+
<div className="flex-1 bg-white">
10+
<iframe
11+
src="https://calendar.google.com/calendar/embed?src=en.usa%23holiday%40group.v.calendar.google.com&ctz=America%2FNew_York"
12+
style={{ border: 0 }}
13+
width="100%"
14+
height="100%"
15+
frameBorder="0"
16+
scrolling="no"
17+
title="Google Calendar"
18+
></iframe>
19+
</div>
20+
</div>
21+
);
22+
};
23+
24+
export default CalendarPanel;

src/components/Layout.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useState } from 'react';
2+
import Sidebar from './Sidebar';
3+
import { PanelLeft, PanelRight } from 'lucide-react';
4+
import { useApp } from '../context/AppContext';
5+
6+
interface LayoutProps {
7+
children: React.ReactNode;
8+
rightPanel: React.ReactNode;
9+
}
10+
11+
const Layout: React.FC<LayoutProps> = ({ children, rightPanel }) => {
12+
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
13+
const [rightPanelOpen, setRightPanelOpen] = useState(false);
14+
const { projects, activeProjectId } = useApp();
15+
16+
const activeProject = projects.find(p => p.id === activeProjectId);
17+
18+
return (
19+
<div className="flex h-screen w-screen bg-gray-900 text-white overflow-hidden">
20+
{/* Left Panel */}
21+
<div
22+
className={`transition-all duration-300 ease-in-out border-r border-gray-700 flex flex-col ${leftPanelOpen ? 'w-64' : 'w-0 opacity-0 overflow-hidden'
23+
}`}
24+
>
25+
<Sidebar />
26+
</div>
27+
28+
{/* Main Content */}
29+
<div className="flex-1 flex flex-col min-w-0 bg-gray-800 relative">
30+
{/* Header / Toolbar */}
31+
<div className="h-14 border-b border-gray-700 flex items-center px-4 justify-between bg-gray-900/50 backdrop-blur shrink-0">
32+
<button
33+
onClick={() => setLeftPanelOpen(!leftPanelOpen)}
34+
className="p-2 hover:bg-gray-700 rounded-md transition-colors"
35+
title="Toggle Sidebar"
36+
>
37+
<PanelLeft size={20} />
38+
</button>
39+
40+
<div className="flex flex-col justify-center flex-1 mx-4 overflow-hidden">
41+
{activeProject ? (
42+
<>
43+
<h1 className="font-bold text-2xl leading-none truncate w-full text-left mb-0.5">{activeProject.name}</h1>
44+
{activeProject.description && (
45+
<span className="text-sm text-gray-400 truncate w-full text-left leading-none">{activeProject.description}</span>
46+
)}
47+
</>
48+
) : (
49+
<div className="font-semibold text-lg text-gray-500">No Project Selected</div>
50+
)}
51+
</div>
52+
53+
<button
54+
onClick={() => setRightPanelOpen(!rightPanelOpen)}
55+
className="p-2 hover:bg-gray-700 rounded-md transition-colors"
56+
title="Toggle Calendar"
57+
>
58+
<PanelRight size={20} />
59+
</button>
60+
</div>
61+
62+
{/* Content Area */}
63+
<div className="flex-1 overflow-auto p-6">
64+
{children}
65+
</div>
66+
</div>
67+
68+
{/* Right Panel */}
69+
<div
70+
className={`transition-all duration-300 ease-in-out border-l border-gray-700 bg-gray-900 flex flex-col ${rightPanelOpen ? 'w-96' : 'w-0 opacity-0 overflow-hidden'
71+
}`}
72+
>
73+
{rightPanel}
74+
</div>
75+
</div>
76+
);
77+
};
78+
79+
export default Layout;

src/components/Modal.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useEffect } from 'react';
2+
import { X } from 'lucide-react';
3+
4+
interface ModalProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
title: string;
8+
children: React.ReactNode;
9+
footer?: React.ReactNode;
10+
}
11+
12+
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, footer }) => {
13+
useEffect(() => {
14+
const handleEsc = (e: KeyboardEvent) => {
15+
if (e.key === 'Escape') onClose();
16+
};
17+
if (isOpen) {
18+
window.addEventListener('keydown', handleEsc);
19+
}
20+
return () => window.removeEventListener('keydown', handleEsc);
21+
}, [isOpen, onClose]);
22+
23+
if (!isOpen) return null;
24+
25+
return (
26+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
27+
<div className="bg-gray-800 border border-gray-700 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden animate-in zoom-in-95 duration-200">
28+
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700 bg-gray-900/50">
29+
<h3 className="text-lg font-semibold text-white">{title}</h3>
30+
<button
31+
onClick={onClose}
32+
className="p-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
33+
>
34+
<X size={20} />
35+
</button>
36+
</div>
37+
38+
<div className="p-4">
39+
{children}
40+
</div>
41+
42+
{footer && (
43+
<div className="px-4 py-3 bg-gray-900/50 border-t border-gray-700 flex justify-end gap-2">
44+
{footer}
45+
</div>
46+
)}
47+
</div>
48+
</div>
49+
);
50+
};
51+
52+
export default Modal;

0 commit comments

Comments
 (0)