@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemeall/create-word-pages",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a Word Pages Obsidian-to-GitHub-Pages starter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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) => response.json())
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 <main className="shell"><p>{status}</p></main>
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
- setStatus("Saving")
75
- const response = await fetch("/api/config", {
76
- method: "POST",
77
- headers: { "Content-Type": "application/json" },
78
- body: JSON.stringify(config)
79
- })
80
- setStatus(response.ok ? "Saved" : "Save failed")
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}>Save</button>
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
- if (req.method === "GET") {
17
- const body = await readFile(configPath, "utf8")
18
- res.setHeader("Content-Type", "application/json")
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(JSON.stringify({ ok: true }))
33
- })
34
- return
35
- }
20
+ res.end(body)
21
+ return
22
+ }
36
23
 
37
- res.statusCode = 405
38
- res.end("Method not allowed")
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
  }