Build Guide (ft. Electron)
Setup Your Environment
You need to make sure you have:
- Node.js and npm
- A Hackatime compatible IDE like Visual Studio Code
- Git
Getting Started
Follow these steps:
mkdir mijn-journal cd mijn-journal npm init -y npm install electron --save-dev
Project Structure
Your Electron project should have these files:
- package.json - Project dependencies
- index.js - Main Electron process
- index.html - Your app's UI
- renderer.js - Renderer process
- preload.js - Security bridge between main and renderer
- style.css - Styling
Create Your App
index.html
This is your app's main interface !! The HTML page your users will see.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Mijn Journal</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="window"> <div class="title-bar"> <div class="title-bar-text">Mijn Journal</div> </div> <textarea id="entry" placeholder="Thinking...."></textarea> <br /> <button onclick="saveEntry()">Save Entry</button> <ul id="entriesList"></ul> </div> <script src="renderer.js"></script> </body> </html>
style.css
This file helps you fix the looks of your app using plain old CSS!!
@font-face { font-family: "Pixelated MS Sans Serif"; src: url("https://unpkg.com/pixelated-ms-sans-serif/build/pixelated-ms-sans-serif.woff2") format("woff2"); src: url("https://unpkg.com/pixelated-ms-sans-serif/build/pixelated-ms-sans-serif.woff") format("woff"); font-weight: normal; font-style: normal; } body { font-family: "Pixelated MS Sans Serif", "Tahoma", sans-serif; font-size: 16px; margin: 0; padding: 0; background: #008080; color: #000; } .window { margin: 30px; background: #c0c0c0; border: 2px solid; border-top-color: #fff; border-left-color: #fff; border-right-color: #808080; border-bottom-color: #808080; padding: 10px; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5); } .title-bar { background: #000080; color: #fff; padding: 3px 5px; font-weight: bold; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .title-bar-text { padding-left: 5px; } .title-bar-controls button { background: #c0c0c0; border: 1px solid; border-top-color: #fff; border-left-color: #fff; border-right-color: #808080; border-bottom-color: #808080; font-family: "Marlett", "Arial", sans-serif; padding: 0; margin: 0 1px; width: 16px; height: 14px; min-width: 0; } textarea { width: calc(100% - 10px); font-size: 14px; border: 2px inset #808080; resize: vertical; background: #fff; padding: 3px; margin-top: 10px; min-height: 100px; } button { margin-top: 10px; padding: 6px 12px; cursor: pointer; background: #c0c0c0; border: 1px outset #fff; border-right-color: #000; border-bottom-color: #000; box-shadow: 1px 1px 0px #808080; font-family: inherit; } button:active { border-style: inset; box-shadow: none; } ul { list-style-type: none; padding: 0; margin: 10px 0 0 0; border: 2px inset #808080; background: #fff; padding: 5px; max-height: 200px; overflow-y: auto; } li { margin: 4px 0; padding: 5px; border: 1px dotted #ccc; cursor: pointer; } li:hover { background: #000080; color: #fff; }
index.js
Think of this as the "backend" of your app. It's the very first thing that runs and acts as the main process. It can do stuf like create windows, interact with your OS, and, importantly, access the file system to save and load data. For security. This is the only place where you should be writing code that needs direct access to system resources please :3
const { app, BrowserWindow, ipcMain, Notification } = require("electron"); const path = require("path"); const fs = require("fs"); let entriesDir; function createWindow() { const win = new BrowserWindow({ width: 600, height: 500, webPreferences: { preload: path.join(__dirname, "preload.js"), }, }); win.loadFile("index.html"); } app.whenReady().then(() => { const userDataPath = app.getPath("userData"); entriesDir = path.join(userDataPath, "entries"); if (!fs.existsSync(entriesDir)) { fs.mkdirSync(entriesDir, { recursive: true }); } createWindow(); }); ipcMain.handle("save-entry", async (_, content) => { try { const filename = `entry-${Date.now()}.txt`; fs.writeFileSync(path.join(entriesDir, filename), content, "utf-8"); return "Entry saved successfully!"; } catch (err) { console.error("Error saving entry", err); return "Failed to save entry"; } }); ipcMain.handle("load-entries", () => { try { return fs.readdirSync(entriesDir).map((file) => fs.readFileSync(path.join(entriesDir, file), "utf-8")); } catch (err) { console.error("Error loading entries", err); return []; } }); ipcMain.on("open-entry-window", (event, content) => { const entryWin = new BrowserWindow({ width: 400, height: 400, title: "Journal Entry", parent: BrowserWindow.getFocusedWindow(), modal: true, }); const htmlContent = ` <!DOCTYPE html> <html><head><title>Entry</title><style>body{font-family:sans-serif;padding:20px;white-space:pre-wrap;}</style></head> <body>${content.replace(//g, ">")}</body> </html>`; entryWin.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(htmlContent)); }); ipcMain.handle("show-notification", (_, title, body) => { new Notification({ title, body }).show(); });
preload.js
This is the secure
bridge between the "backend" (index.js) and the "frontend" (renderer.js).
For
security, the code in your window (renderer.js) cannot directly access the
stuff in the backend. The preload script runs in a special, maybe
"privileged" context that allows it to talk to the main process. Here, we
use the contextBridge
to safely expose specific functions from
the main process (like saving and loading files) to the frontend code
without exposing dangerous modules to the web content.
const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('journalAPI', { save: (content) => ipcRenderer.invoke('save-entry', content), load: () => ipcRenderer.invoke('load-entries'), openEntry: (content) => ipcRenderer.send("open-entry-window", content), });
renderer.js
This is the "frontend"
JavaScript for your app, running directly within (index.html
).
It's responsible for everything the user sees and interacts with. It
operates in a kind of sandbox, so instead of accessing stuff directly, it
uses journalAPI
object we safely exposed in the preload.
const sillyQuotes = ["gurt: yo", "six seven", "empl*yment"]; async function saveEntry() { const entry = document.getElementById("entry").value; if (!entry.trim()) { alert("Cannot save an empty entry!"); return; } const result = await window.journalAPI.save(entry); const randomQuote = sillyQuotes[Math.floor(Math.random() * sillyQuotes.length)]; new Notification("Entry Saved!", { body: randomQuote }); document.getElementById("entry").value = ""; loadEntries(); } async function loadEntries() { const entries = await window.journalAPI.load(); const list = document.getElementById("entriesList"); list.innerHTML = ""; entries.reverse().forEach((content) => { const li = document.createElement("li"); li.textContent = content.slice(0, 50) + (content.length > 50 ? "..." : ""); li.dataset.fullEntry = content; li.addEventListener("click", () => { window.journalAPI.openEntry(li.dataset.fullEntry); }); list.appendChild(li); }); } window.onload = () => { Notification.requestPermission(); loadEntries(); };
Run Your App
Follow these steps to get your app running:
1. Update package.json
In your package.json
, add a start
script to the
scripts
object. This will be the command to run your app. If
you have other scripts like test
, you can add this alongside
them:
"scripts": { "start": "electron ." }
2. Run the app
Start your Electron app:
npm start
Build & Distribute Your App
Ready to share your app with the world? Let's package it into an executable!
1. Install electron-builder
This tool helps you package your Electron app into platform-specific distributables (like .exe for Windows, .dmg for Mac, etc.):
npm install electron-builder --save-dev
2. Update your package.json
Add build configuration and scripts to your package.json
. Add
these sections:
"main": "index.js", "scripts": { "start": "electron .", "build": "electron-builder", "build-win": "electron-builder --win", "build-mac": "electron-builder --mac", "build-linux": "electron-builder --linux" }, "build": { "appId": "com.yourname.mijn-journal", "productName": "Mijn Journal", "directories": { "output": "dist" }, "files": [ "index.js", "index.html", "renderer.js", "preload.js", "style.css", "package.json" ], "win": { "target": "nsis", "icon": "icon.ico" }, "mac": { "target": "dmg", "icon": "icon.icns" }, "linux": { "target": "AppImage", "icon": "icon.png" } }
3. Build Your App
Create executables for your platform:
# Build for your current platform npm run build # Build specifically for Windows npm run build-win # Build specifically for Mac npm run build-mac # Build specifically for Linux npm run build-linux
Windows Tip: If your build fails with an error like "A required privilege is not held by the client" or "Cannot create symbolic link", it's because Windows needs administrator rights to do this. You can run your terminal as an Administrator and try again.
5. Find Your Built App
After building, you'll find your executables in the dist/
folder:
- Windows: .exe installer and unpacked folder
- Mac: .dmg file
- Linux: .AppImage file
6. Upload to GitHub
- Push your code to GitHub
- Go to your repo → Releases → Create a new release
- Upload the built files from
dist/
as release assets - Write release notes explaining what your app does
- Publish the release!