@codemeall/create-word-pages 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from "react"
|
|
|
2
2
|
import { createRoot } from "react-dom/client"
|
|
3
3
|
import "./styles.css"
|
|
4
4
|
|
|
5
|
+
type SaveState = "idle" | "dirty" | "saving" | "saved" | "failed"
|
|
6
|
+
|
|
5
7
|
type WordPagesConfig = {
|
|
6
8
|
site: {
|
|
7
9
|
title: string
|
|
@@ -34,15 +36,26 @@ function displayPagesUrl(baseUrl: string) {
|
|
|
34
36
|
function App() {
|
|
35
37
|
const [config, setConfig] = useState<WordPagesConfig | null>(null)
|
|
36
38
|
const [status, setStatus] = useState("Loading")
|
|
39
|
+
const [saveState, setSaveState] = useState<SaveState>("idle")
|
|
40
|
+
const [message, setMessage] = useState("Load the wizard, edit settings, then save.")
|
|
41
|
+
const [lastSavedAt, setLastSavedAt] = useState("")
|
|
37
42
|
|
|
38
43
|
useEffect(() => {
|
|
39
44
|
fetch("/api/config")
|
|
40
|
-
.then((response) =>
|
|
45
|
+
.then((response) => {
|
|
46
|
+
if (!response.ok) throw new Error("Could not load word-pages.config.json")
|
|
47
|
+
return response.json()
|
|
48
|
+
})
|
|
41
49
|
.then((data) => {
|
|
42
50
|
setConfig(data)
|
|
43
51
|
setStatus("Ready")
|
|
52
|
+
setMessage("Configuration loaded from word-pages.config.json.")
|
|
53
|
+
})
|
|
54
|
+
.catch((error) => {
|
|
55
|
+
setStatus("Could not load configuration")
|
|
56
|
+
setSaveState("failed")
|
|
57
|
+
setMessage(error instanceof Error ? error.message : "Restart npm run wizard from the site root.")
|
|
44
58
|
})
|
|
45
|
-
.catch(() => setStatus("Could not load configuration"))
|
|
46
59
|
}, [])
|
|
47
60
|
|
|
48
61
|
const pageUrl = useMemo(() => {
|
|
@@ -51,7 +64,16 @@ function App() {
|
|
|
51
64
|
}, [config])
|
|
52
65
|
|
|
53
66
|
if (!config) {
|
|
54
|
-
return
|
|
67
|
+
return (
|
|
68
|
+
<main className="shell">
|
|
69
|
+
<section className={`status-panel ${saveState}`} role="status" aria-live="polite">
|
|
70
|
+
<div>
|
|
71
|
+
<strong>{status}</strong>
|
|
72
|
+
<p>{message}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
</main>
|
|
76
|
+
)
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
function updateSite<K extends keyof WordPagesConfig["site"]>(key: K, value: WordPagesConfig["site"][K]) {
|
|
@@ -68,16 +90,36 @@ function App() {
|
|
|
68
90
|
: current.site.baseUrl
|
|
69
91
|
}
|
|
70
92
|
})
|
|
93
|
+
setSaveState("dirty")
|
|
94
|
+
setMessage("Unsaved changes. Click Save changes to write word-pages.config.json.")
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
async function save() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
try {
|
|
99
|
+
setStatus("Saving")
|
|
100
|
+
setSaveState("saving")
|
|
101
|
+
setMessage("Saving word-pages.config.json...")
|
|
102
|
+
|
|
103
|
+
const response = await fetch("/api/config", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify(config)
|
|
107
|
+
})
|
|
108
|
+
const result = await response.json().catch(() => ({ message: "No response body" }))
|
|
109
|
+
if (!response.ok || result.ok === false) {
|
|
110
|
+
throw new Error(result.message ?? "Save failed")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const savedAt = new Date().toLocaleTimeString()
|
|
114
|
+
setStatus("Saved")
|
|
115
|
+
setSaveState("saved")
|
|
116
|
+
setLastSavedAt(savedAt)
|
|
117
|
+
setMessage(`Saved to word-pages.config.json at ${savedAt}. Run npm run preview in another terminal to rebuild the site.`)
|
|
118
|
+
} catch (error) {
|
|
119
|
+
setStatus("Save failed")
|
|
120
|
+
setSaveState("failed")
|
|
121
|
+
setMessage(error instanceof Error ? error.message : "Save failed")
|
|
122
|
+
}
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
return (
|
|
@@ -88,7 +130,17 @@ function App() {
|
|
|
88
130
|
<h1>Configure your Obsidian-powered site.</h1>
|
|
89
131
|
<p className="lede">This wizard writes local configuration only. It does not ask for GitHub credentials and does not upload your vault.</p>
|
|
90
132
|
</div>
|
|
91
|
-
<button onClick={save}>
|
|
133
|
+
<button type="button" onClick={save} disabled={saveState === "saving"}>
|
|
134
|
+
{saveState === "saving" ? "Saving..." : "Save changes"}
|
|
135
|
+
</button>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<section className={`status-panel ${saveState}`} role="status" aria-live="polite">
|
|
139
|
+
<div>
|
|
140
|
+
<strong>{status}</strong>
|
|
141
|
+
<p>{message}</p>
|
|
142
|
+
</div>
|
|
143
|
+
{lastSavedAt && <span>Last saved {lastSavedAt}</span>}
|
|
92
144
|
</section>
|
|
93
145
|
|
|
94
146
|
<section className="grid">
|
|
@@ -133,6 +185,27 @@ function App() {
|
|
|
133
185
|
<p><strong>Expected Pages URL:</strong> {pageUrl}</p>
|
|
134
186
|
</section>
|
|
135
187
|
|
|
188
|
+
<section className="next-steps">
|
|
189
|
+
<h2>Next steps</h2>
|
|
190
|
+
<div className="steps-grid">
|
|
191
|
+
<div>
|
|
192
|
+
<span>1</span>
|
|
193
|
+
<h3>Edit content</h3>
|
|
194
|
+
<p>Open <code>content/</code> in Obsidian and update pages, posts, and notes.</p>
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
<span>2</span>
|
|
198
|
+
<h3>Preview locally</h3>
|
|
199
|
+
<p>Run <code>npm run preview</code> in a second terminal. Quartz serves the site at its local preview URL.</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<span>3</span>
|
|
203
|
+
<h3>Publish</h3>
|
|
204
|
+
<p>Commit, push to GitHub, and enable Pages with GitHub Actions.</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
136
209
|
<footer>
|
|
137
210
|
<span>{status}</span>
|
|
138
211
|
<span>Next: open <code>content/</code> in Obsidian, then run <code>npm run preview</code>.</span>
|
|
@@ -59,6 +59,48 @@ button {
|
|
|
59
59
|
cursor: pointer;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
button:disabled {
|
|
63
|
+
cursor: wait;
|
|
64
|
+
opacity: 0.72;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.status-panel {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
gap: 16px;
|
|
72
|
+
margin: 24px 0 0;
|
|
73
|
+
border: 1px solid #d8d0c0;
|
|
74
|
+
border-left: 5px solid #8a8376;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 16px;
|
|
77
|
+
background: #fffdf8;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.status-panel p {
|
|
81
|
+
margin: 4px 0 0;
|
|
82
|
+
color: #5f594f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.status-panel.saved {
|
|
86
|
+
border-left-color: #28786f;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.status-panel.dirty,
|
|
90
|
+
.status-panel.saving {
|
|
91
|
+
border-left-color: #8d5c2c;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.status-panel.failed {
|
|
95
|
+
border-left-color: #b83232;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.status-panel > span {
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
color: #6f675c;
|
|
101
|
+
font-size: 0.92rem;
|
|
102
|
+
}
|
|
103
|
+
|
|
62
104
|
.grid {
|
|
63
105
|
display: grid;
|
|
64
106
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
@@ -105,6 +147,44 @@ textarea {
|
|
|
105
147
|
margin-top: 0;
|
|
106
148
|
}
|
|
107
149
|
|
|
150
|
+
.next-steps {
|
|
151
|
+
margin-top: 24px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.steps-grid {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
157
|
+
gap: 16px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.steps-grid > div {
|
|
161
|
+
border: 1px solid #ded8ca;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
padding: 18px;
|
|
164
|
+
background: #fffdf8;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.steps-grid span {
|
|
168
|
+
display: inline-grid;
|
|
169
|
+
width: 28px;
|
|
170
|
+
height: 28px;
|
|
171
|
+
place-items: center;
|
|
172
|
+
border-radius: 50%;
|
|
173
|
+
color: white;
|
|
174
|
+
background: #28786f;
|
|
175
|
+
font-size: 0.9rem;
|
|
176
|
+
font-weight: 800;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.steps-grid h3 {
|
|
180
|
+
margin: 14px 0 8px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.steps-grid p {
|
|
184
|
+
margin: 0;
|
|
185
|
+
color: #5f594f;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
code {
|
|
109
189
|
border-radius: 4px;
|
|
110
190
|
padding: 2px 5px;
|
|
@@ -121,12 +201,18 @@ footer {
|
|
|
121
201
|
|
|
122
202
|
@media (max-width: 720px) {
|
|
123
203
|
.hero,
|
|
124
|
-
footer
|
|
204
|
+
footer,
|
|
205
|
+
.status-panel {
|
|
125
206
|
align-items: stretch;
|
|
126
207
|
flex-direction: column;
|
|
127
208
|
}
|
|
128
209
|
|
|
129
|
-
.grid
|
|
210
|
+
.grid,
|
|
211
|
+
.steps-grid {
|
|
130
212
|
grid-template-columns: 1fr;
|
|
131
213
|
}
|
|
214
|
+
|
|
215
|
+
.status-panel > span {
|
|
216
|
+
white-space: normal;
|
|
217
|
+
}
|
|
132
218
|
}
|
|
@@ -13,29 +13,48 @@ export default defineConfig({
|
|
|
13
13
|
name: "word-pages-config-api",
|
|
14
14
|
configureServer(server) {
|
|
15
15
|
server.middlewares.use("/api/config", async (req, res) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
res.end(body)
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (req.method === "POST") {
|
|
24
|
-
let raw = ""
|
|
25
|
-
req.on("data", (chunk) => {
|
|
26
|
-
raw += chunk
|
|
27
|
-
})
|
|
28
|
-
req.on("end", async () => {
|
|
29
|
-
const parsed = JSON.parse(raw)
|
|
30
|
-
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
16
|
+
try {
|
|
17
|
+
if (req.method === "GET") {
|
|
18
|
+
const body = await readFile(configPath, "utf8")
|
|
31
19
|
res.setHeader("Content-Type", "application/json")
|
|
32
|
-
res.end(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
20
|
+
res.end(body)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
if (req.method === "POST") {
|
|
25
|
+
let raw = ""
|
|
26
|
+
req.on("data", (chunk) => {
|
|
27
|
+
raw += chunk
|
|
28
|
+
})
|
|
29
|
+
req.on("end", async () => {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw)
|
|
32
|
+
await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`)
|
|
33
|
+
res.setHeader("Content-Type", "application/json")
|
|
34
|
+
res.end(JSON.stringify({ ok: true, path: configPath }))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
res.statusCode = 500
|
|
37
|
+
res.setHeader("Content-Type", "application/json")
|
|
38
|
+
res.end(JSON.stringify({
|
|
39
|
+
ok: false,
|
|
40
|
+
message: error instanceof Error ? error.message : "Could not save configuration"
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.statusCode = 405
|
|
48
|
+
res.setHeader("Content-Type", "application/json")
|
|
49
|
+
res.end(JSON.stringify({ ok: false, message: "Method not allowed" }))
|
|
50
|
+
} catch (error) {
|
|
51
|
+
res.statusCode = 500
|
|
52
|
+
res.setHeader("Content-Type", "application/json")
|
|
53
|
+
res.end(JSON.stringify({
|
|
54
|
+
ok: false,
|
|
55
|
+
message: error instanceof Error ? error.message : "Configuration API failed"
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
39
58
|
})
|
|
40
59
|
}
|
|
41
60
|
}
|