This commit is contained in:
Mohammad Jarrar 2026-03-15 07:36:20 +03:00
parent 59b5807601
commit dec3e85fe2
11 changed files with 1008 additions and 0 deletions

View File

View File

@ -0,0 +1,25 @@
# ⚡ Electron.js Crash Course
**University:**
**Semester:**
**Students:** ~__ | Background: Basic HTML/CSS
---
## 📋 Sessions
| # | Title | Status |
| --- | ----------------------------------------------- | ------- |
| 1 | [[Session 1 - How Electron Works]] | ✅ Ready |
| 2 | [[Session 2 - JavaScript Bootcamp]] | ✅ Ready |
| 3 | [[Session 3 - Windows Menus & Native Features]] | ✅ Ready |
| 4 | [[Session 4 - IPC Communication]] | ✅ Ready |
| 5 | [[Session 5 - App Design & WebSockets Intro]] | ✅ Ready |
| 6 | [[Session 6 - WebSocket Server]] | ✅ Ready |
| 7 | [[Session 7 - Connecting UI to WebSocket]] | ✅ Ready |
| 8 | [[Session 8 - Packaging & Distribution]] | ✅ Ready |
---
## 🏁 Final Project
[[Final Project - LAN Chat App]]

View File

@ -0,0 +1,126 @@
---
tags: [electron, setup, architecture]
session: 1
duration: 2 hours
status: ready
---
# Session 1 — The Desktop Web: How Electron Works
## 🎯 Objectives
- Understand what Electron is and why it exists
- Set up the development environment
- Run a working Electron app
- Understand the Main vs Renderer process model
---
## 🧠 Concepts Covered
### What is Electron?
Electron lets you build desktop apps using HTML, CSS, and JavaScript.
It bundles two things together:
- **Chromium** → renders your UI (like a built-in browser)
- **Node.js** → gives you access to the filesystem, OS, and more
> Real apps built with Electron: VS Code, Slack, Discord, Figma, Notion
### Desktop vs Web Apps
| Web App | Desktop App |
|---------|-------------|
| Runs in browser | Runs natively on OS |
| No file system access | Full file system access |
| URL-based | Has its own window/icon |
| Auto-updated | Needs packaging/installer |
### The Two-Process Model ⚠️ (Most Important Concept)
```
┌─────────────────────────────┐
│ MAIN PROCESS │ ← Node.js world
│ - Creates windows │ File system, OS APIs
│ - App lifecycle │
│ - Background logic │
└────────────┬────────────────┘
│ IPC (messages)
┌────────────▼────────────────┐
│ RENDERER PROCESS │ ← Browser world
│ - Displays your HTML/CSS │ DOM, UI events
│ - One per window │
└─────────────────────────────┘
```
**Key rule:** Renderer cannot directly access Node.js. They communicate via IPC (covered in Session 4).
---
## 🖥 Talking Points
1. Open VS Code and show the folder structure of a blank Electron app
2. Walk through `package.json` — point out `"main": "main.js"`
3. Open `main.js` — explain `app.whenReady()` and `BrowserWindow`
4. Open `index.html` — show it's just normal HTML
5. Run `npm start` — a window appears!
---
## 💻 Live Demo
```js
// main.js — bare minimum Electron app
const { app, BrowserWindow } = require('electron')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
```
```html
<!-- index.html -->
<!DOCTYPE html>
<html>
<head><title>My First App</title></head>
<body>
<h1>Hello Electron! 👋</h1>
</body>
</html>
```
---
## 🛠 Hands-on Exercise (45 min)
**Goal:** Get a window running and customize it
1. Install Node.js from nodejs.org
2. Run in terminal:
```bash
mkdir my-electron-app
cd my-electron-app
npm init -y
npm install --save-dev electron
```
3. Create `main.js` and `index.html` from the demo above
4. Add to `package.json`: `"start": "electron ."`
5. Run `npm start`
**Challenges (pick one or more):**
- Change the window title
- Change the window size to 400x400
- Add your name in big text to `index.html`
- Set `resizable: false` on the window
---
## 📦 Resources
- [Electron Docs](https://electronjs.org/docs)
- [Node.js Download](https://nodejs.org)
- [[Session 2 - JavaScript Bootcamp]] ← next session
## ✅ Checklist
- [ ] Node.js installed on lab machines
- [ ] Starter repo shared with students
- [ ] Demo code tested on Windows + Mac

View File

@ -0,0 +1,140 @@
---
tags: [javascript, fundamentals, dom]
session: 2
duration: 2 hours
status: ready
---
# Session 2 — JavaScript Crash Bootcamp
## 🎯 Objectives
- Write and understand modern JavaScript
- Manipulate the DOM
- Use Node.js modules with `require`
- Install and use an npm package
---
## 🧠 Concepts Covered
### Variables & Functions
```js
// Prefer const and let over var
const name = "Alice"
let count = 0
// Arrow function
const greet = (name) => {
return `Hello, ${name}!`
}
// Short form
const double = (n) => n * 2
```
### DOM Manipulation
```js
// Select an element
const btn = document.getElementById('myButton')
const title = document.querySelector('h1')
// Change content
title.textContent = "New Title"
title.style.color = "blue"
// Listen for events
btn.addEventListener('click', () => {
console.log('Button clicked!')
})
```
### Node.js Modules
```js
// Built-in Node module
const path = require('path')
const fs = require('fs')
// Read a file
const content = fs.readFileSync('notes.txt', 'utf-8')
console.log(content)
```
### npm Packages
```bash
npm install moment # installs a package
```
```js
const moment = require('moment')
console.log(moment().format('MMMM Do YYYY'))
```
### Async Basics
```js
// Things that take time use callbacks or async/await
setTimeout(() => {
console.log("This runs after 2 seconds")
}, 2000)
```
---
## 🖥 Talking Points
1. Show the browser DevTools console — students can try JS right there
2. Demo DOM changes live in the browser
3. Show `require` in Node.js vs `import` (keep it simple, use require)
4. Explain why `const` is preferred
---
## 🛠 Hands-on Exercise (50 min)
**Build a click counter app, then load it in Electron**
Part 1 — Build in browser first:
```html
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; text-align: center; padding: 40px; }
button { font-size: 24px; padding: 10px 30px; cursor: pointer; }
#count { font-size: 72px; color: #333; }
</style>
</head>
<body>
<h1>Click Counter</h1>
<div id="count">0</div>
<button id="btn">Click me!</button>
<script>
let clicks = 0
const countEl = document.getElementById('count')
const btn = document.getElementById('btn')
btn.addEventListener('click', () => {
clicks++
countEl.textContent = clicks
if (clicks >= 10) countEl.style.color = 'red'
})
</script>
</body>
</html>
```
Part 2 — Load it in Electron:
- Drop this `index.html` into their Electron project from Session 1
- Run `npm start` — it's now a desktop app!
**Challenges:**
- Add a reset button
- Show a congratulations message at 20 clicks
- Change the background color based on click count
---
## 📦 Resources
- [MDN JavaScript Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
- [[Session 1 - How Electron Works]] ← previous
- [[Session 3 - Windows Menus & Native Features]] ← next
-

View File

@ -0,0 +1,112 @@
---
tags: [electron, BrowserWindow, menus, dialogs, filesystem]
session: 3
duration: 2 hours
status: ready
---
# Session 3 — Windows, Menus & Native Features
## 🎯 Objectives
- Configure BrowserWindow with different options
- Build a native application menu
- Open and save files using system dialogs
- Read and write files with Node.js `fs`
---
## 🧠 Concepts Covered
### BrowserWindow Options
```js
const win = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 400,
title: "My App",
backgroundColor: '#1e1e1e',
frame: true, // set false for frameless window
resizable: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
```
### App Lifecycle Events
```js
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
```
### Native Menus
```js
const { Menu, MenuItem } = require('electron')
const menu = Menu.buildFromTemplate([
{
label: 'File',
submenu: [
{ label: 'Open', accelerator: 'CmdOrCtrl+O', click: openFile },
{ label: 'Save', accelerator: 'CmdOrCtrl+S', click: saveFile },
{ type: 'separator' },
{ role: 'quit' }
]
},
{ role: 'editMenu' },
{ role: 'viewMenu' }
])
Menu.setApplicationMenu(menu)
```
### File Dialogs & fs
```js
const { dialog } = require('electron')
const fs = require('fs')
async function openFile() {
const result = await dialog.showOpenDialog({
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
})
if (!result.canceled) {
const content = fs.readFileSync(result.filePaths[0], 'utf-8')
// send content to renderer via IPC (next session!)
}
}
```
---
## 🛠 Hands-on Exercise — Markdown Notepad (60 min)
Build a simple text editor:
**Features to implement:**
1. A `<textarea>` that fills the window
2. File → Open loads a `.txt` file into the textarea
3. File → Save writes the textarea content back to disk
4. Title bar shows the filename
**Starter structure:**
```
notepad/
├── main.js
├── preload.js
├── index.html
└── package.json
```
> ⚠️ Note: Full IPC wiring comes in Session 4 — for now, students can use `remote` module or handle everything in main.js as a simplified demo
---
## 📦 Resources
- [BrowserWindow Docs](https://www.electronjs.org/docs/latest/api/browser-window)
- [dialog Docs](https://www.electronjs.org/docs/latest/api/dialog)
- [[Session 4 - IPC Communication]] ← next (this is where it all clicks)

View File

@ -0,0 +1,101 @@
---
tags: [electron, ipc, preload, contextBridge, security]
session: 4
duration: 2 hours
status: ready
---
# Session 4 — IPC: Making the Two Processes Talk
## 🎯 Objectives
- Understand why IPC exists and why it's necessary
- Send messages from Renderer → Main and back
- Use `preload.js` and `contextBridge` safely
- Wire a full two-way communication flow
---
## 🧠 Concepts Covered
### Why IPC?
The Renderer runs in a sandboxed browser context.
It cannot directly call `fs.readFile()` or access Node.js.
IPC is the secure message-passing bridge.
```
Renderer → ipcRenderer.send('open-file') →
Main → ipcMain.on('open-file', handler) →
Main → win.webContents.send('file-content', data) →
Renderer → ipcRenderer.on('file-content', handler)
```
### The Secure Way: preload.js + contextBridge
```js
// preload.js — runs in a special context with BOTH Node and DOM access
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('open-file'),
saveFile: (content) => ipcRenderer.invoke('save-file', content),
onFileLoaded: (callback) => ipcRenderer.on('file-loaded', callback)
})
```
```js
// main.js — handles the requests
const { ipcMain, dialog } = require('electron')
const fs = require('fs')
ipcMain.handle('open-file', async () => {
const result = await dialog.showOpenDialog({ ... })
if (!result.canceled) {
return fs.readFileSync(result.filePaths[0], 'utf-8')
}
})
ipcMain.handle('save-file', async (event, content) => {
const result = await dialog.showSaveDialog({})
if (!result.canceled) {
fs.writeFileSync(result.filePath, content)
return true
}
})
```
```js
// renderer (index.html script) — uses the exposed API
document.getElementById('open-btn').addEventListener('click', async () => {
const content = await window.electronAPI.openFile()
document.getElementById('editor').value = content
})
```
### invoke vs send
| `ipcRenderer.send` | `ipcRenderer.invoke` |
|--------------------|----------------------|
| Fire and forget | Returns a Promise |
| One-way | Two-way (request/response) |
| Use for events | Use for data fetching |
---
## 🛠 Hands-on Exercise — Wire the Notepad (60 min)
Complete the Notepad from Session 3 using proper IPC:
1. Set up `preload.js` with `contextBridge`
2. Add `webPreferences: { preload: ... }` to BrowserWindow
3. Expose `openFile` and `saveFile` via `electronAPI`
4. Wire up Open and Save buttons in the renderer
5. **Bonus:** Add a word count display that updates as you type
---
## ⚠️ Common Mistakes
- Forgetting to set `preload` in `webPreferences`
- Using `nodeIntegration: true` (insecure, avoid it)
- Trying to `require('fs')` in renderer directly
---
## 📦 Resources
- [IPC Docs](https://www.electronjs.org/docs/latest/tutorial/ipc)
- [contextBridge Docs](https://www.electronjs.org/docs/latest/api/context-bridge)
- [[Session 5 - App Design & WebSockets Intro]] ← next

View File

@ -0,0 +1,120 @@
---
tags: [websockets, planning, design, project]
session: 5
duration: 2 hours
status: ready
---
# Session 5 — What Are We Building? + WebSockets Intro
## 🎯 Objectives
- Decide on the final project as a class
- Understand what WebSockets are and when to use them
- Sketch the app UI and define features
- Set up the project repo
---
## 🧠 Part 1 — Choosing the App (30 min)
### What makes a good Electron + WebSocket project?
- Real-time updates (needs WebSockets)
- Benefits from being a desktop app (not just a website)
- Simple enough to build in 3 sessions
- Visual and satisfying to demo
### Proposed Options
| App Idea | WebSocket Use | Complexity |
|----------|---------------|------------|
| 🗨 LAN Chat App | Send/receive messages in real-time | ⭐⭐ |
| 📝 Collaborative Notes | Multiple users edit the same note | ⭐⭐⭐ |
| 📊 Live System Dashboard | Show CPU/memory data live | ⭐⭐ |
| 🎮 Multiplayer Quiz Game | Questions pushed to all clients | ⭐⭐⭐ |
> 💡 **Recommended:** LAN Chat App — no internet needed, instantly testable in class, clearly demonstrates WebSocket concepts
### Class Vote → Decision: _______________
---
## 🧠 Part 2 — WebSockets Explained (30 min)
### HTTP vs WebSocket
```
HTTP (request/response):
Client → "Give me the messages" → Server
Server → "Here are 3 messages" → Client
(connection closes)
WebSocket (persistent):
Client ←→ Server (connection stays open)
Server can push data anytime!
```
### When to use WebSockets vs HTTP
| Use HTTP | Use WebSockets |
|----------|----------------|
| Load a page | Live chat |
| Submit a form | Real-time notifications |
| Fetch data once | Multiplayer games |
| REST API calls | Live dashboards |
### WebSocket Lifecycle
```
1. Client sends HTTP upgrade request
2. Server agrees → connection upgrades to WS
3. Both sides can now send messages freely
4. Either side can close the connection
```
---
## 🛠 Part 3 — Design the App (60 min)
**For LAN Chat App:**
Sketch on paper / whiteboard first:
- What does the UI look like?
- What data does each message contain?
- What happens when someone connects / disconnects?
**Message structure:**
```json
{
"type": "message",
"username": "Alice",
"text": "Hello everyone!",
"timestamp": "14:32"
}
```
**Set up the project:**
```bash
mkdir lan-chat-app
cd lan-chat-app
npm init -y
npm install --save-dev electron
npm install ws
```
**File structure to set up today:**
```
lan-chat-app/
├── main.js
├── preload.js
├── package.json
└── renderer/
├── index.html
├── style.css
└── app.js
```
Build the static HTML/CSS shell of the chat UI (no functionality yet)
---
## 📦 Resources
- [WebSocket MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
- [ws npm package](https://www.npmjs.com/package/ws)
- [[Session 6 - WebSocket Server]] ← next

View File

@ -0,0 +1,139 @@
---
tags: [websockets, server, nodejs, ws]
session: 6
duration: 2 hours
status: ready
---
# Session 6 — Building the WebSocket Server
## 🎯 Objectives
- Set up a `ws` WebSocket server inside Electron's Main process
- Handle client connections and disconnections
- Receive and broadcast messages to all clients
- Test with two laptops communicating in real-time
---
## 🧠 Concepts Covered
### The `ws` Package
```bash
npm install ws
```
### Basic WebSocket Server
```js
const { WebSocketServer } = require('ws')
const wss = new WebSocketServer({ port: 3000 })
wss.on('connection', (socket) => {
console.log('A client connected!')
socket.on('message', (data) => {
const message = JSON.parse(data)
console.log('Received:', message)
// Broadcast to ALL connected clients
wss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(JSON.stringify(message))
}
})
})
socket.on('close', () => {
console.log('A client disconnected')
})
})
console.log('WebSocket server running on ws://localhost:3000')
```
### Integrating Into Electron's Main Process
```js
// main.js
const { app, BrowserWindow } = require('electron')
const { WebSocketServer } = require('ws')
let wss
function startServer() {
wss = new WebSocketServer({ port: 3000 })
wss.on('connection', (socket) => {
socket.on('message', (data) => {
// Broadcast to all
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(data.toString())
}
})
})
})
}
app.whenReady().then(() => {
startServer()
createWindow()
})
```
### Connecting From Another Machine (LAN)
```js
// On another laptop, connect to the server's IP
const ws = new WebSocket('ws://192.168.1.X:3000')
// Replace X with the server machine's local IP
```
> Find your IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
---
## 🛠 Hands-on Exercise (70 min)
**Goal: Two laptops, one chat**
1. Student A sets up the WebSocket server in `main.js`
2. Student B opens a plain HTML file and connects to Student A's IP
3. Student B types a message → it appears on Student A's console
4. Add broadcasting so it goes back to all clients
**Test script for Student B (plain HTML):**
```html
<!DOCTYPE html>
<html>
<body>
<input id="msg" placeholder="Type a message">
<button onclick="send()">Send</button>
<div id="log"></div>
<script>
const ws = new WebSocket('ws://REPLACE_WITH_IP:3000')
ws.onmessage = (event) => {
const log = document.getElementById('log')
log.innerHTML += `<p>${event.data}</p>`
}
function send() {
const text = document.getElementById('msg').value
ws.send(JSON.stringify({ username: 'Student B', text }))
}
</script>
</body>
</html>
```
---
## ⚠️ Common Issues
- Firewall blocking port 3000 → disable Windows Defender firewall temporarily
- Wrong IP address → double check with `ipconfig`
- Port already in use → change to `3001`
---
## 📦 Resources
- [ws Documentation](https://github.com/websockets/ws)
- [[Session 7 - Connecting UI to WebSocket]] ← next

View File

@ -0,0 +1,124 @@
---
tags: [websockets, ipc, renderer, UI, final-project]
session: 7
duration: 2 hours
status: ready
---
# Session 7 — Connecting the UI to the WebSocket
## 🎯 Objectives
- Connect the Renderer UI to the WebSocket via IPC
- Display incoming messages dynamically in the DOM
- Send messages from the input field
- Handle connection states and polish the UX
---
## 🧠 Architecture Recap
```
[Renderer UI]
↕ ipcRenderer (via preload.js)
[Main Process]
↕ WebSocket
[Other Clients]
```
The Renderer doesn't touch WebSocket directly — it talks to Main via IPC, and Main relays to/from the WebSocket server.
---
## 🧠 Concepts Covered
### Forwarding WebSocket Messages via IPC
```js
// main.js — when a WS message arrives, forward to renderer
wss.on('connection', (socket) => {
socket.on('message', (data) => {
// Broadcast to all WS clients
wss.clients.forEach(client => {
if (client.readyState === 1) client.send(data.toString())
})
// Also send to our own renderer window
mainWindow.webContents.send('new-message', JSON.parse(data.toString()))
})
})
```
### Exposing the API via preload.js
```js
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('chatAPI', {
sendMessage: (message) => ipcRenderer.send('send-message', message),
onMessage: (callback) => ipcRenderer.on('new-message', (_, msg) => callback(msg))
})
```
### Renderer: Sending & Displaying Messages
```js
// renderer/app.js
const form = document.getElementById('message-form')
const input = document.getElementById('message-input')
const chatBox = document.getElementById('chat-box')
// Send a message
form.addEventListener('submit', (e) => {
e.preventDefault()
const text = input.value.trim()
if (!text) return
window.chatAPI.sendMessage({
username: localStorage.getItem('username') || 'Anonymous',
text,
timestamp: new Date().toLocaleTimeString()
})
input.value = ''
})
// Receive messages
window.chatAPI.onMessage((msg) => {
const div = document.createElement('div')
div.className = 'message'
div.innerHTML = `
<span class="username">${msg.username}</span>
<span class="text">${msg.text}</span>
<span class="time">${msg.timestamp}</span>
`
chatBox.appendChild(div)
chatBox.scrollTop = chatBox.scrollHeight
})
```
### Connection State UI
```js
// Show "connecting..." / "connected" / "disconnected" states
ipcRenderer.on('ws-status', (_, status) => {
document.getElementById('status').textContent = status
document.getElementById('status').className = status
})
```
---
## 🛠 Hands-on Exercise — Complete the App (80 min)
1. Wire up `preload.js` with `chatAPI`
2. Implement the message send handler
3. Implement the message receive + DOM append
4. Style the chat bubbles in `style.css`
5. Add username prompt on app start (`prompt()` or a simple input screen)
6. Test across 3+ laptops on the same network
**Bonus challenges:**
- Show "User X joined" when someone connects
- Different color bubbles for your messages vs others
- Sound notification on new message
- Timestamp on each message
---
## 📦 Resources
- [[Session 6 - WebSocket Server]] ← previous
- [[Session 8 - Packaging & Distribution]] ← next

View File

@ -0,0 +1,115 @@
---
tags: [electron, packaging, electron-builder, distribution]
session: 8
duration: 2 hours
status: ready
---
# Session 8 — Packaging, Distribution & Wrap-up
## 🎯 Objectives
- Package the app into a native installer
- Create `.exe` (Windows), `.dmg` (Mac), `.AppImage` (Linux)
- Understand what comes after (updates, crash reporting)
- Demo day — run each other's packaged apps
---
## 🧠 Concepts Covered
### electron-builder
```bash
npm install --save-dev electron-builder
```
Add to `package.json`:
```json
{
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"build": {
"appId": "com.yourname.lanchat",
"productName": "LAN Chat",
"directories": {
"output": "dist"
},
"win": {
"target": "nsis"
},
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage"
}
}
}
```
Then run:
```bash
npm run build
```
Output appears in `/dist` folder.
### App Icons
- Create a 512x512 `.png` icon
- Save as `build/icon.png` (electron-builder picks it up automatically)
- Tools: [icon.kitchen](https://icon.kitchen) or Figma
### What's Inside the Package?
```
dist/
├── win-unpacked/ ← the app folder
├── LAN Chat Setup.exe ← installer
└── builder-effective-config.yaml
```
---
## 🧠 What's Next After This Course?
### Electron Alternatives
| Tool | Language | Why Use It |
|------|----------|------------|
| **Tauri** | Rust + Web | Much smaller bundle size |
| **NW.js** | Web | Older, similar to Electron |
| **Flutter** | Dart | Cross-platform inc. mobile |
### Electron Features to Explore
- **Auto-updater** — push updates silently to users
- **Tray icon** — app lives in system tray
- **Notifications** — native OS notifications
- **Deep links** — open app from a URL
- **Crash reporter** — collect error reports
---
## 🛠 Demo Day (60 min)
**Each student / pair:**
1. Runs `npm run build`
2. Shares the installer via USB or local network share
3. Another student installs it on a fresh machine
4. Everyone's chat app connects to one room
**Presentation (2 min each):**
- What feature are you most proud of?
- What was the hardest bug you fixed?
- What would you add with more time?
---
## ✅ Course Wrap-up Checklist
- [ ] All students have a working packaged app
- [ ] Share the vault / notes repo with students
- [ ] Collect feedback form
- [ ] Share "what to learn next" resources
## 📦 Final Resources
- [electron-builder Docs](https://www.electron.build)
- [Awesome Electron](https://github.com/sindresorhus/awesome-electron)
- [[Session 1 - How Electron Works]] ← back to the beginning

6
content/index.md Normal file
View File

@ -0,0 +1,6 @@
---
title: Welcome to Quartz
---
This is a blank Quartz installation.
See the [documentation](https://quartz.jzhao.xyz) for how to get started.