Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cms.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ commit_author_name = "Git CMS"
commit_author_email = "git-cms@example.com"

[content]
roots = ["content/**", "pages/**", "data/**/*.json", "data/**/*.yaml", "data/**/*.yml", "README.md"]
roots = ["content/**"]
max_file_bytes = 1048576
editable_extensions = ["md", "mdx", "json", "yaml", "yml", "txt"]

Expand Down
2 changes: 1 addition & 1 deletion cms.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ commit_author_name = "Git CMS"
commit_author_email = "git-cms@example.com"

[content]
roots = ["content/**", "pages/**", "data/**/*.json", "data/**/*.yaml", "data/**/*.yml", "README.md"]
roots = ["content/**"]
max_file_bytes = 1048576
editable_extensions = ["md", "mdx", "json", "yaml", "yml", "txt"]

Expand Down
10 changes: 8 additions & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ <h1>Content editor</h1>
<button id="sign-out" class="link-button" type="button">Sign out</button>
</div>
<form id="save-form">
<label for="file-picker">Existing file</label>
<select id="file-picker" name="file-picker" disabled>
<option value="">Loading available files…</option>
</select>
<small class="hint">Choose an existing file from <code>content/</code> to load its current content.</small>

<label for="filename">File name</label>
<input id="filename" name="filename" placeholder="content/welcome.md" required />
<small class="hint">Use an allowed path, for example <code>content/welcome.md</code>, <code>pages/about.md</code>, or <code>README.md</code>.</small>
<input id="filename" name="filename" placeholder="Select a file above" required readonly />
<small class="hint">The file path is set by the selector and must remain inside <code>content/</code>.</small>

<label for="content">Content</label>
<textarea id="content" name="content" rows="16" placeholder="Write your content here…"></textarea>
Expand Down
61 changes: 60 additions & 1 deletion frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ const signedOut = document.querySelector("#signed-out");
const editor = document.querySelector("#editor");
const user = document.querySelector("#user");
const form = document.querySelector("#save-form");
const filePicker = document.querySelector("#file-picker");
const filename = document.querySelector("#filename");
const content = document.querySelector("#content");
const save = document.querySelector("#save");
const status = document.querySelector("#status");
let fileLoadRequest = 0;

function setStatus(message, isError = false) {
status.textContent = message;
Expand Down Expand Up @@ -56,8 +58,62 @@ async function showEditor() {
user.textContent = `Signed in as ${me.login} · ${me.repository}`;
signedOut.hidden = true;
editor.hidden = false;
await loadAvailableFiles();
}

async function listFiles(path = "") {
const query = path ? `?path=${encodeURIComponent(path)}` : "";
const result = await api(`/api/files${query}`);
const entries = await Promise.all(
result.entries.map(async (entry) => {
if (entry.kind === "directory") return listFiles(entry.path);
return [entry];
}),
);
return entries.flat();
}

async function loadAvailableFiles() {
filePicker.disabled = true;
filePicker.innerHTML = '<option value="">Loading available files…</option>';
try {
const files = await listFiles("content");
files.sort((a, b) => a.path.localeCompare(b.path));
filePicker.innerHTML = '<option value="">Select a file…</option>';
for (const file of files) {
const option = document.createElement("option");
option.value = file.path;
option.textContent = file.path;
filePicker.append(option);
}
filePicker.disabled = files.length === 0;
if (files.length === 0) setStatus("No editable files are available.");
} catch (error) {
filePicker.innerHTML = '<option value="">Unable to load files</option>';
setStatus(`Could not load available files: ${error.message}`, true);
}
}

filePicker.addEventListener("change", async () => {
const path = filePicker.value;
if (!path) return;

const request = ++fileLoadRequest;
filePicker.disabled = true;
setStatus(`Loading ${path}…`);
try {
const file = await api(`/api/files/${apiPath(path)}`);
if (request !== fileLoadRequest) return;
filename.value = file.path;
content.value = file.content;
setStatus(`Loaded ${file.path}.`);
} catch (error) {
if (request === fileLoadRequest) setStatus(`Could not load ${path}: ${error.message}`, true);
} finally {
if (request === fileLoadRequest) filePicker.disabled = false;
}
});

async function getSession(path, initialContent) {
const existing = sessionStorage.getItem(sessionKey);
if (existing) return { session: JSON.parse(existing), created: false };
Expand All @@ -80,7 +136,10 @@ async function getSession(path, initialContent) {
form.addEventListener("submit", async (event) => {
event.preventDefault();
const path = filename.value.trim();
if (!path) return;
if (!path.startsWith("content/")) {
setStatus("Select a file from content/ before saving.", true);
return;
}

save.disabled = true;
setStatus("Saving…");
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ form { display: grid; gap: 9px; }
label { font-weight: 650; }
.hint { margin-top: -4px; color: #667085; font-size: .82rem; }
code { padding: 1px 4px; background: #f2f4f7; border-radius: 4px; }
input, textarea { width: 100%; padding: 11px 12px; border: 1px solid #cfd4dc; border-radius: 8px; font: inherit; }
input, select, textarea { width: 100%; padding: 11px 12px; border: 1px solid #cfd4dc; border-radius: 8px; font: inherit; }
textarea { resize: vertical; line-height: 1.5; }
input:focus, textarea:focus { outline: 3px solid #dcd7ff; border-color: #6956d6; }
input:focus, select:focus, textarea:focus { outline: 3px solid #dcd7ff; border-color: #6956d6; }
.button { display: inline-block; width: fit-content; margin-top: 10px; padding: 10px 15px; background: #5b47cb; color: white; border: 0; border-radius: 8px; font: inherit; font-weight: 650; text-decoration: none; cursor: pointer; }
.button:hover { background: #4937ae; }
.button:disabled { opacity: .65; cursor: wait; }
Expand Down