Skip to content

Commit

Permalink
✨ added simple pinning system
Browse files Browse the repository at this point in the history
  • Loading branch information
loloToster committed Aug 31, 2024
1 parent a5414d3 commit b2c6c4a
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 34 deletions.
64 changes: 54 additions & 10 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const db = new DataBase(dbPath, async err => {
`ALTER TABLE items ADD COLUMN folder TEXT DEFAULT NULL`,
)

await dbRunAsync(
db,
`ALTER TABLE items ADD COLUMN pinned INTEGER DEFAULT 0`,
)

app.listen(PORT, () => {
console.log("Listening on port", PORT)
})
Expand All @@ -93,8 +98,9 @@ function addFiles(files: Express.Multer.File[], ip: string, folder?: string, fol
title: folder,
ip,
folder: folderId ?? null, // todo: check if folder exists
created_at: new Date(),
trashed: 0
created_at: new Date().getTime(),
trashed: 0,
pinned: 0
}

await new Promise<void>((r, rj) => {
Expand Down Expand Up @@ -127,8 +133,9 @@ function addFiles(files: Express.Multer.File[], ip: string, folder?: string, fol
title: file.originalname,
ip,
folder: newFolder?.id ?? folderId ?? null,
created_at: new Date(),
trashed: 0
created_at: new Date().getTime(),
trashed: 0,
pinned: 0
})
} catch (err) {
console.error(err)
Expand Down Expand Up @@ -185,13 +192,14 @@ apiRouter.post("/text", jsonParser, async (req, res) => {
ip: getClientIp(req) || "unknown",
text: req.body.text,
folder: req.body.folderId ?? null, // todo: check if folder exists
created_at: new Date(),
trashed: 0
created_at: new Date().getTime(),
trashed: 0,
pinned: 0
}

let params = Object.values(newItem)
// remove created_at & trashed
params.splice(-2)
// remove created_at & trashed & pinned
params.splice(-3)

db.run(
`INSERT INTO items(type, id, title, ip, text, folder) VALUES (?, ?, ?, ?, ?, ?)`,
Expand Down Expand Up @@ -257,6 +265,23 @@ apiRouter.patch(
}
)

apiRouter.patch(
["/item/:id/pin", "/item/:id/unpin"],
async (req, res) => {
const { id } = req.params
const pinned = req.path.includes("unpin") ? 0 : 1

db.run(
`UPDATE items SET pinned=${pinned} WHERE id = ?`,
[id],
async err => {
if (err) return res.status(500).send()
res.send()
}
)
}
)

async function getFolderIds(ids: string[]): Promise<string[]> {
return new Promise<any[]>((res, rej) => {
const idsPlaceholders = "?, ".repeat(ids.length).slice(0, -2)
Expand Down Expand Up @@ -332,8 +357,8 @@ apiRouter.get("/items", async (req, res) => {

db.all(
trashed === "true" ?
`SELECT * FROM items WHERE trashed = 1 ${textSearch} ORDER BY created_at DESC` :
`SELECT * FROM items WHERE folder IS NULL AND trashed = 0 ${textSearch} ORDER BY created_at DESC`
`SELECT * FROM items WHERE trashed = 1 ${textSearch} ORDER BY pinned DESC, created_at DESC` :
`SELECT * FROM items WHERE folder IS NULL AND trashed = 0 ${textSearch} ORDER BY pinned DESC, created_at DESC`
,
[q],
(err, rows) => {
Expand Down Expand Up @@ -408,6 +433,25 @@ apiRouter.patch(["/items/trash", "/items/restore"], jsonParser, (req, res) => {
)
})

apiRouter.patch(["/items/pin", "/items/unpin"], jsonParser, (req, res) => {
if (!validateIdsBody(req.body)) {
return res.status(400).send()
}

const pinned = req.path.includes("unpin") ? 0 : 1
const { ids } = req.body
const idsPlaceholders = "?, ".repeat(ids.length).slice(0, -2)

db.run(
`UPDATE items SET pinned=${pinned} WHERE id IN (${idsPlaceholders})`,
ids,
err => {
if (err) return res.status(500).send()
res.send()
}
)
})

apiRouter.delete("/items", jsonParser, async (req, res) => {
if (!validateIdsBody(req.body)) {
return res.status(400).send()
Expand Down
27 changes: 27 additions & 0 deletions client/src/components/FileItem/FileItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
display: flex;
}

@media (hover: none) {
&__options {
display: flex;
}
}

&__metadata {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -162,4 +168,25 @@
}
}
}

&__pinned {
position: absolute;
top: 6px;
left: 6px;
border-radius: 50%;
background-color: #212121dd;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;

svg {
fill: c.$main;
}

.file-item:hover & {
display: none;
}
}
}
48 changes: 44 additions & 4 deletions client/src/components/FileItem/FileItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useEffect, useState } from "react"
import { Link } from "react-router-dom"

import "./FileItem.scss"
Expand All @@ -10,18 +10,34 @@ function FileItem(props: {
onDelete: Function,
onRestore: Function,
onSelect: Function,
onRangeSelect: Function
onRangeSelect: Function,
onPin: Function,
onUnpin: Function
}) {
const {
fileItem,
onDelete,
onRestore,
onSelect,
onRangeSelect
onRangeSelect,
onPin,
onUnpin
} = props

const [moreOpen, setMoreOpen] = useState(false)

useEffect(() => {
const onWindowClick = (e: MouseEvent) => {
const clickOnItem = e.composedPath().some(el => (el as HTMLElement).classList?.contains(`item-${fileItem.id}`))
if (clickOnItem) return

setMoreOpen(false)
}

window.addEventListener("click", onWindowClick)
return () => window.removeEventListener("click", onWindowClick)
}, [setMoreOpen, fileItem.id])

const handleClick = (e: React.MouseEvent) => {
if (!e.ctrlKey && !e.shiftKey) return

Expand All @@ -36,6 +52,16 @@ function FileItem(props: {
e.preventDefault()
}

const handlePin = () => {
onPin(fileItem.id)
setMoreOpen(false)
}

const handleUnpin = () => {
onUnpin(fileItem.id)
setMoreOpen(false)
}

const handleRestore = async (e: React.MouseEvent) => {
e.preventDefault()
onRestore(fileItem.id)
Expand All @@ -50,7 +76,7 @@ function FileItem(props: {
const icon = isImg ? `/cdn/${fileItem.id}` : `/icon/${fileItem.title}`

return (
<div className={`item file-item ${isImg ? "" : "file-item--with-icon"} ${fileItem.selected ? "file-item--selected" : ""}`}>
<div className={`item item-${fileItem.id} file-item ${isImg ? "" : "file-item--with-icon"} ${fileItem.selected ? "file-item--selected" : ""}`}>
<Link
onClick={e => handleClick(e)}
to={`/file/${fileItem.id}`}
Expand Down Expand Up @@ -98,6 +124,13 @@ function FileItem(props: {
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m256-200-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>
<div className="file-item__more__wrapper">
{fileItem.pinned ?
(
<button onClick={handleUnpin}>Unpin</button>
) : (
<button onClick={handlePin}>Pin</button>
)
}
<a
href={`/cdn/${fileItem.id}`}
download={fileItem.title}>
Expand All @@ -119,6 +152,13 @@ function FileItem(props: {
</button>
</div>
</div>
{fileItem.pinned && (
<div className="file-item__pinned">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
<path d="m624-480 96 96v72H516v228l-36 36-36-36v-228H240v-72l96-96v-264h-48v-72h384v72h-48v264Z" />
</svg>
</div>
)}
</div>
)
}
Expand Down
43 changes: 38 additions & 5 deletions client/src/components/ItemList/ItemList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ function ItemList(props: Props) {
setFolderModalOpen(false)
}

const pinItems = (ids: string[], pin = false) =>
setItems(prev => prev.map(i => ids.includes(i.id) ? ({ ...i, pinned: pin ? 1 : 0 }) : i))

const rmItem = (id: string) => setItems(prev => prev.filter(i => i.id !== id))
const rmItems = (ids: string[]) => setItems(prev => prev.filter(i => !ids.includes(i.id)))

Expand Down Expand Up @@ -204,18 +207,42 @@ function ItemList(props: Props) {
let res = await fetch(`/api/item/${id}/${apiPath}`, { method })
if (res.ok) rmItem(id)
}
}

const handleItemPin = async (id: string, type: "pin" | "unpin") => {
if (items.find(i => i.id === id)?.selected) {
const selectedIds = items.filter(i => i.selected).map(i => i.id)

let res = await fetch(`/api/items/${type}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ids: selectedIds })
})

if (res.ok) pinItems(selectedIds, type === "pin")
} else {
let res = await fetch(`/api/item/${id}/${type}`, { method: "PATCH" })
if (res.ok) pinItems([id], type === "pin")
}
}

const handlePin = (id: string) => {
handleItemPin(id, "pin")
}

const handleUnpin = (id: string) => {
handleItemPin(id, "unpin")
}

const handleTrash = async (id: string) => {
const handleTrash = (id: string) => {
handleItemRemoval(id, "trash")
}

const handleRestore = async (id: string) => {
const handleRestore = (id: string) => {
handleItemRemoval(id, "restore")
}

const handleDelete = async (id: string) => {
const handleDelete = (id: string) => {
handleItemRemoval(id, "delete")
}

Expand Down Expand Up @@ -266,13 +293,19 @@ function ItemList(props: Props) {
return true

return false
}).map(item => {
}).sort(
(a, b) => b.created_at - a.created_at
).sort(
(a, b) => b.pinned - a.pinned
).map(item => {
const itemProps = {
key: item.id,
onRestore: handleRestore,
onDelete: item.trashed ? handleDelete : handleTrash,
onSelect: handleSelect,
onRangeSelect: handleRangeSelect
onRangeSelect: handleRangeSelect,
onPin: handlePin,
onUnpin: handleUnpin
}

if (item.type === "text")
Expand Down
25 changes: 17 additions & 8 deletions client/src/components/TextItem/TextItem.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,24 @@
}

&__title {
display: block;
overflow: hidden;
width: 100%;
line-height: 110%;
font-size: 1rem;
text-overflow: ellipsis;
display: flex;
gap: 0.3ch;

::spelling-error {
text-decoration: none;
svg {
fill: c.$main;
}

input {
display: block;
overflow: hidden;
width: 100%;
line-height: 110%;
font-size: 1rem;
text-overflow: ellipsis;

::spelling-error {
text-decoration: none;
}
}
}

Expand Down
Loading

0 comments on commit b2c6c4a

Please sign in to comment.