Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
loloToster committed Nov 26, 2023
2 parents 44c7568 + d38d7a1 commit c6005d8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 36 deletions.
8 changes: 6 additions & 2 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,18 @@ apiRouter.get("/item/:id", async (req, res) => {
)
})

const supportedPatchFields: Record<string, string | undefined> = {
title: "string",
text: "string"
}

apiRouter.patch("/item/:id", express.json(), async (req, res) => {
const { id } = req.params

const { field, value } = req.body

if (
!["title"].includes(field) ||
typeof value !== "string"
typeof value !== supportedPatchFields[field]
) {
return res.status(400).send()
}
Expand Down
21 changes: 17 additions & 4 deletions client/src/components/TextItem/TextItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,25 @@

&__text {
flex: 1 1 0;
padding-right: 4px;
overflow-y: auto;
word-wrap: break-word;
min-height: 0;

pre {
pre,
textarea {
overflow: auto;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
padding-right: 4px;
background-color: c.$bg;
white-space: pre;
font-size: 1rem;
}

textarea {
border: none;
outline: none;
resize: none;
}
}
}
126 changes: 96 additions & 30 deletions client/src/components/TextItem/TextItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, MouseEvent, useState, ChangeEvent } from "react"
import { useRef, useState, ChangeEvent } from "react"
import useDebounce from "src/hooks/useDebounce"
import useAfterMountEffect from "src/hooks/useAfterMountEffect"

Expand Down Expand Up @@ -60,8 +60,9 @@ function urlify(text: string) {
function TextItem(props: { textItem: TextJson, onDelete: Function, onRestore: Function }) {
const { textItem, onDelete, onRestore } = props

const copyBtn = useRef<HTMLButtonElement>(null)
const [text, setText] = useState(textItem.text)
const [title, setTitle] = useState(textItem.title || "No Title")

const debouncedTitle = useDebounce(title)

useAfterMountEffect(() => {
Expand All @@ -79,13 +80,48 @@ function TextItem(props: { textItem: TextJson, onDelete: Function, onRestore: Fu
setTitle(e.target.value)
}

const [editing, setEditing] = useState(false)
const [textareaVal, setTextareaVal] = useState(textItem.text)
const textarea = useRef<HTMLTextAreaElement>(null)

const handleEditStart = () => {
setEditing(true)
setTimeout(() => {
if (!textarea.current) return

textarea.current.focus()
textarea.current.selectionStart = textarea.current.selectionEnd = textarea.current.value.length
textarea.current.scrollTo({ top: textarea.current.scrollHeight })
})
}

const handleEditCancel = () => {
setEditing(false)
setTextareaVal(text)
}

const handleEditSave = async () => {
setText(textareaVal)
setEditing(false)

fetch(`/api/item/${textItem.id}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
field: "text",
value: textareaVal
})
})
}

const copyBtn = useRef<HTMLButtonElement>(null)
let copyTimeout: any
const handleCopy = (e: MouseEvent) => {
const handleCopy = () => {
const button = copyBtn.current
if (!button) return

clearTimeout(copyTimeout)
copyToClipboard(textItem.text)
copyToClipboard(text)
button.classList.remove("success")
// restarts color animation
void button.querySelector("svg:nth-child(2)")?.scrollHeight
Expand All @@ -102,36 +138,66 @@ function TextItem(props: { textItem: TextJson, onDelete: Function, onRestore: Fu
<input type="text" className="text-item__title" value={title} onChange={handleTitleChange}/>
<div className="text-item__user">{textItem.ip}</div>
</div>
<button onClick={() => onDelete(textItem.id)} title="Delete Text">
{textItem.trashed ? (
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="m9.4 16.5 2.6-2.6 2.6 2.6 1.4-1.4-2.6-2.6L16 9.9l-1.4-1.4-2.6 2.6-2.6-2.6L8 9.9l2.6 2.6L8 15.1ZM7 21q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM7 6v13Z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M7 21q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM9 17h2V8H9Zm4 0h2V8h-2ZM7 6v13Z"></path>
</svg>
)}
</button>
{textItem.trashed ? (
<button onClick={() => onRestore(textItem.id)} title="Restore Text">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M11 16h2v-4.15l1.6 1.55L16 12l-4-4-4 4 1.4 1.4 1.6-1.55Zm-4 5q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM7 6v13Z" />
</svg>
</button>
{editing ? (
<>
<button onClick={handleEditCancel} title="Cancel Edit">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24">
<path d="m256 856-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/>
</svg>
</button>
<button onClick={handleEditSave} title="Save Edit">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24">
<path d="M382 816 154 588l57-57 171 171 367-367 57 57-424 424Z"/>
</svg>
</button>
</>
) : (
<button onClick={handleCopy} ref={copyBtn} title="Copy Text">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M5 22q-.825 0-1.413-.587Q3 20.825 3 20V6h2v14h11v2Zm4-4q-.825 0-1.412-.587Q7 16.825 7 16V4q0-.825.588-1.413Q8.175 2 9 2h9q.825 0 1.413.587Q20 3.175 20 4v12q0 .825-.587 1.413Q18.825 18 18 18Zm0-2h9V4H9v12Zm0 0V4v12Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="m9.55 18.55-6.3-6.3 1.875-1.875L9.55 14.8l9.375-9.375L20.8 7.3Z" />
</svg>
</button>
<>
<button onClick={handleEditStart} title="Edit Text">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24">
<path d="M200 856h56l345-345-56-56-345 345v56Zm572-403L602 285l56-56q23-23 56.5-23t56.5 23l56 56q23 23 24 55.5T829 396l-57 57Zm-58 59L290 936H120V766l424-424 170 170Zm-141-29-28-28 56 56-28-28Z"/>
</svg>
</button>
<button onClick={() => onDelete(textItem.id)} title="Delete Text">
{textItem.trashed ? (
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="m9.4 16.5 2.6-2.6 2.6 2.6 1.4-1.4-2.6-2.6L16 9.9l-1.4-1.4-2.6 2.6-2.6-2.6L8 9.9l2.6 2.6L8 15.1ZM7 21q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM7 6v13Z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M7 21q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM9 17h2V8H9Zm4 0h2V8h-2ZM7 6v13Z"></path>
</svg>
)}
</button>
{textItem.trashed ? (
<button onClick={() => onRestore(textItem.id)} title="Restore Text">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M11 16h2v-4.15l1.6 1.55L16 12l-4-4-4 4 1.4 1.4 1.6-1.55Zm-4 5q-.825 0-1.412-.587Q5 19.825 5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413Q17.825 21 17 21ZM17 6H7v13h10ZM7 6v13Z" />
</svg>
</button>
) : (
<button onClick={handleCopy} ref={copyBtn} title="Copy Text">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="M5 22q-.825 0-1.413-.587Q3 20.825 3 20V6h2v14h11v2Zm4-4q-.825 0-1.412-.587Q7 16.825 7 16V4q0-.825.588-1.413Q8.175 2 9 2h9q.825 0 1.413.587Q20 3.175 20 4v12q0 .825-.587 1.413Q18.825 18 18 18Zm0-2h9V4H9v12Zm0 0V4v12Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
<path d="m9.55 18.55-6.3-6.3 1.875-1.875L9.55 14.8l9.375-9.375L20.8 7.3Z" />
</svg>
</button>
)}
</>
)}
</div>
<div className="text-item__text">
<pre dangerouslySetInnerHTML={{ __html: urlify(textItem.text) }}></pre>
{editing ? (
<textarea ref={textarea}
value={textareaVal}
onChange={e => setTextareaVal((e.target as HTMLTextAreaElement).value)}
placeholder="Type in your text here...">
</textarea>
) : (
<pre dangerouslySetInnerHTML={{ __html: urlify(text) }}></pre>
)}
</div>
</div>)
}
Expand Down

0 comments on commit c6005d8

Please sign in to comment.