
Currently it isn't possible to edit the title of a bookmark (for example, when bookmarking a Measurements Set) for 2 reasons: 1. The modal closes as soon as you click in the title text edit box. 2. The created bookmark uses the original title, ignoring edits made to it. This PR fixes both issues. Co-authored-by: Benjamin Fan <ben-git@swinglonga.com> Reviewed-on: https://codeberg.org/freesewing/freesewing/pulls/366 Reviewed-by: Joost De Cock <joostdecock@noreply.codeberg.org> Co-authored-by: Benjamin Fan <benjamesben@noreply.codeberg.org> Co-committed-by: Benjamin Fan <benjamesben@noreply.codeberg.org>
322 lines
9.9 KiB
JavaScript
322 lines
9.9 KiB
JavaScript
// Dependencies
|
|
import { horFlexClasses, notEmpty } from '@freesewing/utils'
|
|
// Hooks
|
|
import React, { useState, useEffect, Fragment, useContext } from 'react'
|
|
import { useBackend } from '@freesewing/react/hooks/useBackend'
|
|
// Context
|
|
import { LoadingStatusContext } from '@freesewing/react/context/LoadingStatus'
|
|
import { ModalContext } from '@freesewing/react/context/Modal'
|
|
// Components
|
|
import { BookmarkIcon, LeftIcon, PlusIcon, TrashIcon } from '@freesewing/react/components/Icon'
|
|
import { Link as WebLink } from '@freesewing/react/components/Link'
|
|
import { ModalWrapper } from '@freesewing/react/components/Modal'
|
|
import { StringInput } from '@freesewing/react/components/Input'
|
|
|
|
/*
|
|
* Various bookmark types
|
|
*/
|
|
const types = {
|
|
design: 'Designs',
|
|
pattern: 'Patterns',
|
|
set: 'Measurements Sets',
|
|
cset: 'Curated Measurements Sets',
|
|
doc: 'Documentation',
|
|
custom: 'Custom Bookmarks',
|
|
}
|
|
|
|
/**
|
|
* Component for the account/bookmarks page
|
|
*/
|
|
export const Bookmarks = () => {
|
|
// Hooks & Context
|
|
const backend = useBackend()
|
|
const { setModal, clearModal } = useContext(ModalContext)
|
|
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
|
|
|
// State
|
|
const [bookmarks, setBookmarks] = useState([])
|
|
const [selected, setSelected] = useState({})
|
|
const [refresh, setRefresh] = useState(0)
|
|
|
|
// Helper var to see how many are selected
|
|
const selCount = Object.keys(selected).length
|
|
|
|
// Effects
|
|
useEffect(() => {
|
|
const getBookmarks = async () => {
|
|
const [status, body] = await backend.getBookmarks()
|
|
if (status === 200 && body.result === 'success') setBookmarks(body.bookmarks)
|
|
}
|
|
getBookmarks()
|
|
}, [refresh])
|
|
|
|
// Helper method to toggle single selection
|
|
const toggleSelect = (id) => {
|
|
const newSelected = { ...selected }
|
|
if (newSelected[id]) delete newSelected[id]
|
|
else newSelected[id] = 1
|
|
setSelected(newSelected)
|
|
}
|
|
|
|
// Helper method to toggle select all
|
|
const toggleSelectAll = () => {
|
|
if (selCount === bookmarks.length) setSelected({})
|
|
else {
|
|
const newSelected = {}
|
|
for (const bookmark of bookmarks) newSelected[bookmark.id] = 1
|
|
setSelected(newSelected)
|
|
}
|
|
}
|
|
|
|
// Helper to delete one or more bookmarks
|
|
const removeSelectedBookmarks = async () => {
|
|
let i = 0
|
|
for (const id in selected) {
|
|
i++
|
|
await backend.removeBookmark(id)
|
|
setLoadingStatus([
|
|
true,
|
|
<LoadingProgress val={i} max={selCount} msg="Removing Bookmarks" key="linter" />,
|
|
])
|
|
}
|
|
setSelected({})
|
|
setRefresh(refresh + 1)
|
|
setLoadingStatus([true, 'Nailed it', true, true])
|
|
}
|
|
|
|
const perType = {}
|
|
for (const type in types) perType[type] = bookmarks.filter((b) => b.type === type)
|
|
|
|
return (
|
|
<div className="tw:w-full">
|
|
<p className="tw:text-center tw:md:text-right">
|
|
<button
|
|
className="tw:daisy-btn tw:daisy-btn-primary tw:capitalize tw:w-full tw:md:w-auto tw:hover:text-primary-content tw:hover:no-underline"
|
|
onClick={() =>
|
|
setModal(
|
|
<ModalWrapper
|
|
flex="col"
|
|
justify="top lg:justify-center"
|
|
slideFrom="right"
|
|
keepOpenOnClick
|
|
>
|
|
<div className="tw:w-full">
|
|
<h2>New Bookmark</h2>
|
|
<NewBookmark onCreated={() => setRefresh(refresh + 1)} />
|
|
</div>
|
|
</ModalWrapper>
|
|
)
|
|
}
|
|
>
|
|
<PlusIcon />
|
|
New Bookmark
|
|
</button>
|
|
</p>
|
|
{bookmarks.length > 0 ? (
|
|
<button
|
|
className="tw:daisy-btn tw:daisy-btn-error"
|
|
onClick={removeSelectedBookmarks}
|
|
disabled={selCount < 1}
|
|
>
|
|
<TrashIcon /> {selCount} Bookmarks
|
|
</button>
|
|
) : null}
|
|
{Object.entries(types).map(([type, title]) =>
|
|
perType[type].length > 0 ? (
|
|
<Fragment key={type}>
|
|
<h2>{title}</h2>
|
|
<table className="tw:table tw:w-full">
|
|
<thead className="tw:border tw:border-base-300 tw:border-b-2 tw:border-t-0 tw:border-x-0">
|
|
<tr>
|
|
<th className="tw:text-base-300 tw:text-base tw:text-left tw:w-8">
|
|
<input
|
|
type="checkbox"
|
|
className="tw:daisy-checkbox tw:daisy-checkbox-secondary"
|
|
onClick={toggleSelectAll}
|
|
checked={bookmarks.length === selCount}
|
|
/>
|
|
</th>
|
|
<th className="tw:w-1/2">Title</th>
|
|
<th>Location</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{bookmarks
|
|
.filter((bookmark) => bookmark.type === type)
|
|
.map((bookmark, i) => (
|
|
<tr key={i}>
|
|
<td className="tw:text-base tw:font-medium">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected[bookmark.id] ? true : false}
|
|
className="tw:daisy-checkbox tw:daisy-checkbox-secondary"
|
|
onClick={() => toggleSelect(bookmark.id)}
|
|
/>
|
|
</td>
|
|
<td className="tw:text-base tw:font-medium">{bookmark.title}</td>
|
|
<td className="tw:text-base tw:font-medium">
|
|
<WebLink href={bookmark.url}>
|
|
{bookmark.url.length > 30
|
|
? bookmark.url.slice(0, 30) + '...'
|
|
: bookmark.url}
|
|
</WebLink>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</Fragment>
|
|
) : null
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/*
|
|
* Component to create a new bookmark
|
|
*
|
|
* @param {object} props - All the React props
|
|
* @param {function} onCreated - An optional method to call when the bookmark is created
|
|
*/
|
|
export const NewBookmark = ({ onCreated = false }) => {
|
|
// Hooks
|
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
|
const { clearModal } = useContext(ModalContext)
|
|
const backend = useBackend()
|
|
|
|
// State
|
|
const [title, setTitle] = useState('')
|
|
const [url, setUrl] = useState('')
|
|
|
|
// This method will create the bookmark
|
|
const createBookmark = async () => {
|
|
setLoadingStatus([true, 'Processing update'])
|
|
const [status, body] = await backend.createBookmark({
|
|
title,
|
|
url,
|
|
type: 'custom',
|
|
})
|
|
if (status === 201) setLoadingStatus([true, 'Bookmark created', true, true])
|
|
else
|
|
setLoadingStatus([
|
|
true,
|
|
'An error occured, the bookmark was not created. Please report this.',
|
|
true,
|
|
false,
|
|
])
|
|
if (typeof onCreated === 'function') onCreated()
|
|
clearModal()
|
|
}
|
|
|
|
// Render the form
|
|
return (
|
|
<div className="tw:w-full">
|
|
<StringInput
|
|
id="bookmark-title"
|
|
label="Title"
|
|
labelBL="The title/name of your bookmark"
|
|
update={setTitle}
|
|
current={title}
|
|
valid={(val) => val.length > 0}
|
|
placeholder="Bookmark title"
|
|
/>
|
|
<StringInput
|
|
id="bookmark-url"
|
|
label="Location"
|
|
labelBL="The location/url of your bookmark"
|
|
update={setUrl}
|
|
current={url}
|
|
valid={(val) => val.length > 0}
|
|
placeholder={'https://freesewing.org/account'}
|
|
/>
|
|
<div className="tw:flex tw:flex-row tw:gap-2 tw:items-center tw:w-full tw:my-8">
|
|
<button
|
|
className="tw:daisy-btn tw:daisy-btn-primary tw:grow tw:capitalize"
|
|
disabled={!(title.length > 0 && url.length > 0)}
|
|
onClick={createBookmark}
|
|
>
|
|
New bookmark
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/*
|
|
* A component to add a bookmark from wherever
|
|
*
|
|
* @params {object} props - All React props
|
|
* @params {string} props.href - The bookmark href
|
|
* @params {string} props.title - The bookmark title
|
|
* @params {string} props.type - The bookmark type
|
|
*/
|
|
export const BookmarkButton = ({ slug, type, title }) => {
|
|
const { setModal } = useContext(ModalContext)
|
|
const typeTitles = { docs: 'page' }
|
|
|
|
return (
|
|
<button
|
|
className={`tw:daisy-btn tw:daisy-btn-secondary tw:daisy-btn-outline ${horFlexClasses}`}
|
|
onClick={() =>
|
|
setModal(
|
|
<ModalWrapper
|
|
flex="col"
|
|
justify="top lg:justify-center"
|
|
slideFrom="right"
|
|
keepOpenOnClick="true"
|
|
>
|
|
<CreateBookmark {...{ type, title, slug }} />
|
|
</ModalWrapper>
|
|
)
|
|
}
|
|
>
|
|
<BookmarkIcon />
|
|
<span>Bookmark this {typeTitles[type] ? typeTitles[type] : type}</span>
|
|
</button>
|
|
)
|
|
}
|
|
|
|
/*
|
|
* A component to create a bookmark, preloaded with props
|
|
*
|
|
* @params {object} props - All React props
|
|
* @params {string} props.href - The bookmark href
|
|
* @params {string} props.title - The bookmark title
|
|
* @params {string} props.type - The bookmark type
|
|
*
|
|
*/
|
|
export const CreateBookmark = ({ type, title, slug }) => {
|
|
const backend = useBackend()
|
|
const [name, setName] = useState(title)
|
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
|
const { setModal } = useContext(ModalContext)
|
|
|
|
const url = `/${slug}`
|
|
|
|
const bookmark = async (evt) => {
|
|
evt.stopPropagation()
|
|
setLoadingStatus([true, 'Contacting backend'])
|
|
let title = name
|
|
const [status] = await backend.createBookmark({ type, title, url })
|
|
if (status === 201) {
|
|
setLoadingStatus([true, 'Bookmark created', true, true])
|
|
setModal(false)
|
|
} else
|
|
setLoadingStatus([
|
|
true,
|
|
'Something unexpected happened, failed to create a bookmark',
|
|
true,
|
|
false,
|
|
])
|
|
}
|
|
|
|
return (
|
|
<div className="tw:mt-12">
|
|
<h2>New bookmark</h2>
|
|
<StringInput label="Title" current={name} update={setName} valid={notEmpty} labelBL={url} />
|
|
<button className="tw:daisy-btn tw:daisy-btn-primary tw:w-full tw:mt-4" onClick={bookmark}>
|
|
Create bookmark
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|