Guide

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

  1. Push your code to GitHub
  2. Go to your repo → Releases → Create a new release
  3. Upload the built files from dist/ as release assets
  4. Write release notes explaining what your app does
  5. Publish the release!
Start
3:45 PM
need help?
dm @lou
Clippy