Skip to content

Instantly share code, notes, and snippets.

@simonw
Created December 19, 2024 20:23
Show Gist options
  • Save simonw/889e4e8a66382528142c5adf73c29da2 to your computer and use it in GitHub Desktop.
Save simonw/889e4e8a66382528142c5adf73c29da2 to your computer and use it in GitHub Desktop.

Distraction-free Writing Interface with Auto-Save

human (Dec 19, 2024, 12:09 PM)

artifact that gives me a large textarea for writing in which constantly saves what I have entered to localStorage (restoring when I reload the page) and displays a word counter

assistant (Dec 19, 2024, 12:10 PM)

I'll create a simple but effective writing interface with auto-save and word count functionality.

create Writing interface with auto-save

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing interface</title>
<style>
* {
  box-sizing: border-box;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 20px;
  background: #f5f5f5;
}

.container {
  max-width: 800px;
  margin: 0 auto;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.writing-area {
  width: 100%;
  min-height: 400px;
  padding: 20px;
  margin-bottom: 15px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  font-size: 16px;
  line-height: 1.5;
  resize: vertical;
}

.stats {
  color: #666;
  font-size: 14px;
}

.save-status {
  color: #888;
  font-style: italic;
  margin-left: 15px;
}
</style>
</head>
<body>
<div class="container">
  <h1>Writing space</h1>
  <textarea class="writing-area" placeholder="Start writing here..."></textarea>
  <div class="stats">
    Words: <span id="wordCount">0</span>
    <span class="save-status" id="saveStatus"></span>
  </div>
</div>

<script type="module">
const STORAGE_KEY = 'writing-content'
const textarea = document.querySelector('.writing-area')
const wordCountElement = document.getElementById('wordCount')
const saveStatus = document.getElementById('saveStatus')

let saveTimeout

function countWords(text) {
  return text.trim() ? text.trim().split(/\s+/).length : 0
}

function updateWordCount() {
  const count = countWords(textarea.value)
  wordCountElement.textContent = count
}

function showSavedStatus() {
  saveStatus.textContent = 'Saved'
  setTimeout(() => {
    saveStatus.textContent = ''
  }, 2000)
}

function saveToLocalStorage() {
  localStorage.setItem(STORAGE_KEY, textarea.value)
  showSavedStatus()
}

function debouncedSave() {
  clearTimeout(saveTimeout)
  saveStatus.textContent = 'Saving...'
  saveTimeout = setTimeout(saveToLocalStorage, 1000)
}

// Load saved content
const savedContent = localStorage.getItem(STORAGE_KEY)
if (savedContent) {
  textarea.value = savedContent
  updateWordCount()
}

// Event listeners
textarea.addEventListener('input', () => {
  updateWordCount()
  debouncedSave()
})

textarea.addEventListener('paste', () => {
  setTimeout(updateWordCount, 0)
})
</script>
</body>
</html>

I've created a clean and simple writing interface with the following features:

  1. A large textarea that automatically saves to localStorage after 1 second of inactivity
  2. Word counter that updates in real-time
  3. Save status indicator that shows when content is being saved or has been saved
  4. Content restoration on page reload
  5. Mobile-friendly design with proper font sizing
  6. Clean, minimal styling with good contrast

The interface uses vanilla JavaScript with no dependencies and follows the requested styling conventions. Let me know if you'd like any adjustments to the layout or functionality!

human (Dec 19, 2024, 12:17 PM)

Upgrade it to support multiple textareas. Have a button which, when clicked, adds another textarea with its own counter. All of the textareas should be persisted. Have a remove button next to each one too which removes it (and discards the saved text). The remove button should become a "Clear" button if there is only one textarea left, which clears the memory but does not remove the textarea

assistant (Dec 19, 2024, 12:17 PM)

I'll upgrade the interface to support multiple textareas with persistence and removal functionality.

rewrite Untitled

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-textarea writing interface</title>
<style>
* {
  box-sizing: border-box;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 20px;
  background: #f5f5f5;
}

.container {
  max-width: 800px;
  margin: 0 auto;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.writing-section {
  margin-bottom: 30px;
  background: white;
  padding: 20px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.writing-area {
  width: 100%;
  min-height: 200px;
  padding: 20px;
  margin-bottom: 15px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  font-size: 16px;
  line-height: 1.5;
  resize: vertical;
}

.controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.stats {
  color: #666;
  font-size: 14px;
}

.save-status {
  color: #888;
  font-style: italic;
  margin-left: 15px;
}

button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.remove-btn {
  background-color: #ff4444;
  color: white;
}

.remove-btn:hover {
  background-color: #ff6666;
}

.add-btn {
  background-color: #4CAF50;
  color: white;
  margin-top: 20px;
}

.add-btn:hover {
  background-color: #45a049;
}

.clear-btn {
  background-color: #ff9800;
  color: white;
}

.clear-btn:hover {
  background-color: #ffa726;
}
</style>
</head>
<body>
<div class="container">
  <h1>Writing space</h1>
  <div id="writing-container"></div>
  <button class="add-btn" id="addSection">Add new section</button>
</div>

<script type="module">
const STORAGE_KEY = 'writing-sections'
const container = document.getElementById('writing-container')
const addButton = document.getElementById('addSection')

let saveTimeouts = new Map()
let sections = []

function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2)
}

function createSection(id, content = '') {
  const section = document.createElement('div')
  section.className = 'writing-section'
  section.dataset.id = id

  section.innerHTML = `
    <textarea class="writing-area" placeholder="Start writing here...">${content}</textarea>
    <div class="controls">
      <div class="stats">
        Words: <span class="word-count">0</span>
        <span class="save-status"></span>
      </div>
      <button class="remove-btn">Remove</button>
    </div>
  `

  const textarea = section.querySelector('textarea')
  const wordCount = section.querySelector('.word-count')
  const saveStatus = section.querySelector('.save-status')
  const removeBtn = section.querySelector('.remove-btn')

  updateWordCount(textarea, wordCount)

  textarea.addEventListener('input', () => {
    updateWordCount(textarea, wordCount)
    debouncedSave(id, textarea, saveStatus)
  })

  removeBtn.addEventListener('click', () => {
    if (container.children.length === 1) {
      clearSection(id, textarea, saveStatus)
      removeBtn.textContent = 'Clear'
      removeBtn.className = 'clear-btn'
    } else {
      removeSection(id, section)
    }
  })

  return section
}

function countWords(text) {
  return text.trim() ? text.trim().split(/\s+/).length : 0
}

function updateWordCount(textarea, wordCountElement) {
  const count = countWords(textarea.value)
  wordCountElement.textContent = count
}

function showSavedStatus(saveStatus) {
  saveStatus.textContent = 'Saved'
  setTimeout(() => {
    saveStatus.textContent = ''
  }, 2000)
}

function saveToLocalStorage() {
  const sectionsData = Array.from(container.children).map(section => ({
    id: section.dataset.id,
    content: section.querySelector('textarea').value
  }))
  localStorage.setItem(STORAGE_KEY, JSON.stringify(sectionsData))
}

function debouncedSave(id, textarea, saveStatus) {
  if (saveTimeouts.has(id)) {
    clearTimeout(saveTimeouts.get(id))
  }
  saveStatus.textContent = 'Saving...'
  saveTimeouts.set(id, setTimeout(() => {
    saveTimeouts.delete(id)
    saveToLocalStorage()
    showSavedStatus(saveStatus)
  }, 1000))
}

function removeSection(id, section) {
  if (saveTimeouts.has(id)) {
    clearTimeout(saveTimeouts.get(id))
    saveTimeouts.delete(id)
  }
  section.remove()
  saveToLocalStorage()
}

function clearSection(id, textarea, saveStatus) {
  textarea.value = ''
  updateWordCount(textarea, saveStatus.previousElementSibling)
  saveToLocalStorage()
}

// Load saved content
const savedContent = localStorage.getItem(STORAGE_KEY)
if (savedContent) {
  const savedSections = JSON.parse(savedContent)
  savedSections.forEach(section => {
    const newSection = createSection(section.id, section.content)
    container.appendChild(newSection)
  })
} else {
  // Create initial section if no saved content
  const newSection = createSection(generateId())
  container.appendChild(newSection)
}

// Add new section button handler
addButton.addEventListener('click', () => {
  const newSection = createSection(generateId())
  container.appendChild(newSection)
  newSection.querySelector('textarea').focus()
  
  // Update the first/only section's remove button if needed
  if (container.children.length === 2) {
    const firstSection = container.children[0]
    const firstRemoveBtn = firstSection.querySelector('button')
    firstRemoveBtn.textContent = 'Remove'
    firstRemoveBtn.className = 'remove-btn'
  }
})
</script>
</body>
</html>

I've upgraded the interface with the following changes:

  1. Support for multiple writing sections, each with its own:
    • Textarea
    • Word counter
    • Save status indicator
    • Remove/Clear button
  2. New "Add new section" button at the bottom
  3. Persistent storage of all sections using localStorage
  4. Automatic conversion of Remove → Clear button when only one section remains
  5. Each section has a unique ID for proper state management
  6. Improved visual design with section cards and clearer button states

The interface maintains all the original functionality while adding the requested multi-section support. Let me know if you'd like any adjustments to the layout or behavior!

human (Dec 19, 2024, 12:18 PM)

Ensure that when the page first loads if there is only one textarea the button says Clear and not remove - also have that button be disabled if there is no text in the box

assistant (Dec 19, 2024, 12:19 PM)

I'll fix the initial button state and add the disable functionality.

update writing-interface

Find this:

  textarea.addEventListener('input', () => {
    updateWordCount(textarea, wordCount)
    debouncedSave(id, textarea, saveStatus)
  })

Replace with this:

  textarea.addEventListener('input', () => {
    updateWordCount(textarea, wordCount)
    debouncedSave(id, textarea, saveStatus)
    if (container.children.length === 1) {
      removeBtn.disabled = textarea.value.trim().length === 0
    }
  })

update writing-interface

Find this:

.clear-btn:hover {
  background-color: #ffa726;
}

Replace with this:

.clear-btn:hover {
  background-color: #ffa726;
}

.clear-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

update writing-interface

Find this:

function createSection(id, content = '') {

Replace with this:

function createSection(id, content = '', isFirstLoad = false) {

update writing-interface

Find this:

  updateWordCount(textarea, wordCount)

Replace with this:

  updateWordCount(textarea, wordCount)
  
  // Set initial button state
  if (isFirstLoad && container.children.length === 0) {
    removeBtn.textContent = 'Clear'
    removeBtn.className = 'clear-btn'
    removeBtn.disabled = content.trim().length === 0
  }

update writing-interface

Find this:

    const newSection = createSection(section.id, section.content)
    container.appendChild(newSection)

Replace with this:

    const newSection = createSection(section.id, section.content, true)
    container.appendChild(newSection)

update writing-interface

Find this:

  const newSection = createSection(generateId())
  container.appendChild(newSection)

Replace with this:

  const newSection = createSection(generateId(), '', true)
  container.appendChild(newSection)

I've made the following changes:

  1. Added an isFirstLoad parameter to the createSection function to properly handle initial button state
  2. Set the button to "Clear" when there's only one textarea on initial load
  3. Added disabled state styling for the Clear button
  4. Added logic to disable the Clear button when the textarea is empty
  5. The button updates its disabled state in real-time as the user types

Now when you first load the page with a single textarea, you'll see a "Clear" button that's disabled until you start typing. The button state will update automatically as you type or clear the content.

human (Dec 19, 2024, 12:19 PM)

The Clear button should toggle to disabled any time that first textarea is empty, including if the button has been clicked and emptied it

assistant (Dec 19, 2024, 12:19 PM)

I'll update the logic to handle the Clear button's disabled state in all cases where the textarea becomes empty.

update writing-interface

Find this:

function clearSection(id, textarea, saveStatus) {
  textarea.value = ''
  updateWordCount(textarea, saveStatus.previousElementSibling)
  saveToLocalStorage()
}

Replace with this:

function clearSection(id, textarea, saveStatus) {
  textarea.value = ''
  updateWordCount(textarea, saveStatus.previousElementSibling)
  saveToLocalStorage()
  
  // Update clear button state if this is the only section
  if (container.children.length === 1) {
    const clearBtn = textarea.parentElement.querySelector('button')
    clearBtn.disabled = true
  }
}

I've updated the clearSection function to also handle the button's disabled state after clearing. Now the Clear button will become disabled in all cases where the textarea becomes empty:

  1. When the page first loads with an empty textarea
  2. When the user manually deletes all content
  3. When the Clear button is clicked
  4. When there's only one section and its content becomes empty through any means

The button will remain disabled until the user starts typing new content in the textarea.

human (Dec 19, 2024, 12:21 PM)

Let's simplify this a lot: you CAN have 0 textareas, if you do the Add button is still there - so don't bother with the clear button, each one should have a "remove" button instead

Rewrite it all from scratch

assistant (Dec 19, 2024, 12:21 PM)

I'll create a simpler version where textareas can be freely added and removed.

rewrite Untitled

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-textarea writing interface</title>
<style>
* {
  box-sizing: border-box;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 20px;
  background: #f5f5f5;
}

.container {
  max-width: 800px;
  margin: 0 auto;
}

h1 {
  color: #333;
  margin-bottom: 20px;
}

.writing-section {
  margin-bottom: 30px;
  background: white;
  padding: 20px;
  border-radius: 4px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.writing-area {
  width: 100%;
  min-height: 200px;
  padding: 20px;
  margin-bottom: 15px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: white;
  font-size: 16px;
  line-height: 1.5;
  resize: vertical;
}

.controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.stats {
  color: #666;
  font-size: 14px;
}

.save-status {
  color: #888;
  font-style: italic;
  margin-left: 15px;
}

.add-btn {
  background-color: #4CAF50;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.add-btn:hover {
  background-color: #45a049;
}

.remove-btn {
  background-color: #ff4444;
  color: white;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.remove-btn:hover {
  background-color: #ff6666;
}
</style>
</head>
<body>
<div class="container">
  <h1>Writing space</h1>
  <div id="writing-container"></div>
  <button class="add-btn" id="addSection">Add new section</button>
</div>

<script type="module">
const STORAGE_KEY = 'writing-sections'
const container = document.getElementById('writing-container')
const addButton = document.getElementById('addSection')

let saveTimeouts = new Map()

function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2)
}

function createSection(id, content = '') {
  const section = document.createElement('div')
  section.className = 'writing-section'
  section.dataset.id = id

  section.innerHTML = `
    <textarea class="writing-area" placeholder="Start writing here...">${content}</textarea>
    <div class="controls">
      <div class="stats">
        Words: <span class="word-count">0</span>
        <span class="save-status"></span>
      </div>
      <button class="remove-btn">Remove</button>
    </div>
  `

  const textarea = section.querySelector('textarea')
  const wordCount = section.querySelector('.word-count')
  const saveStatus = section.querySelector('.save-status')
  const removeBtn = section.querySelector('.remove-btn')

  updateWordCount(textarea, wordCount)

  textarea.addEventListener('input', () => {
    updateWordCount(textarea, wordCount)
    debouncedSave()
  })

  removeBtn.addEventListener('click', () => {
    section.remove()
    debouncedSave()
  })

  return section
}

function countWords(text) {
  return text.trim() ? text.trim().split(/\s+/).length : 0
}

function updateWordCount(textarea, wordCountElement) {
  const count = countWords(textarea.value)
  wordCountElement.textContent = count
}

function saveToLocalStorage() {
  const sectionsData = Array.from(container.children).map(section => ({
    id: section.dataset.id,
    content: section.querySelector('textarea').value
  }))
  localStorage.setItem(STORAGE_KEY, JSON.stringify(sectionsData))
  
  // Update save status for all sections
  document.querySelectorAll('.save-status').forEach(status => {
    status.textContent = 'Saved'
    setTimeout(() => {
      status.textContent = ''
    }, 2000)
  })
}

function debouncedSave() {
  // Clear any existing save timeout
  if (saveTimeouts.has('save')) {
    clearTimeout(saveTimeouts.get('save'))
  }
  
  // Show 'Saving...' status
  document.querySelectorAll('.save-status').forEach(status => {
    status.textContent = 'Saving...'
  })
  
  // Set new save timeout
  saveTimeouts.set('save', setTimeout(() => {
    saveTimeouts.delete('save')
    saveToLocalStorage()
  }, 1000))
}

// Load saved content
const savedContent = localStorage.getItem(STORAGE_KEY)
if (savedContent) {
  const savedSections = JSON.parse(savedContent)
  savedSections.forEach(section => {
    const newSection = createSection(section.id, section.content)
    container.appendChild(newSection)
  })
}

// Add new section button handler
addButton.addEventListener('click', () => {
  const newSection = createSection(generateId())
  container.appendChild(newSection)
  newSection.querySelector('textarea').focus()
})
</script>
</body>
</html>

I've created a simplified version with the following changes:

  1. Removed the Clear button concept entirely
  2. Each section has a simple Remove button that deletes it
  3. You can now have 0 textareas - the Add button remains at the bottom
  4. Simplified the save status handling to work across all sections
  5. Maintained all core functionality:
    • Word counting
    • Auto-saving
    • Local storage persistence
    • Unique IDs for each section

The interface is now more straightforward and intuitive - you can freely add and remove sections as needed. Let me know if you'd like any adjustments to the layout or functionality!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment