Skip to content
Open
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
27 changes: 25 additions & 2 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ const orderByClause = (by, me, models, type, sub) => {
return 'ORDER BY "Item".boost DESC'
case 'random':
return 'ORDER BY RANDOM()'
case 'custom':
if (type === 'bookmarks') return 'ORDER BY "Bookmark"."custom_order" ASC NULLS LAST, "Bookmark"."created_at" DESC'
break
default:
return `ORDER BY ${type === 'bookmarks' ? '"bookmarkCreatedAt"' : '"Item".created_at'} DESC`
}
Expand Down Expand Up @@ -235,7 +238,7 @@ const relationClause = (type) => {
}

const selectClause = (type) => type === 'bookmarks'
? `${SELECT}, "Bookmark"."created_at" as "bookmarkCreatedAt"`
? `${SELECT}, "Bookmark"."created_at" as "bookmarkCreatedAt", "Bookmark"."custom_order" as "bookmarkCustomOrder"`
: SELECT

const subClauseTable = (type) => COMMENT_TYPE_QUERY.includes(type) ? 'root' : 'Item'
Expand Down Expand Up @@ -421,7 +424,9 @@ export default {
${orderByClause(by, me, models, type)}
OFFSET $4
LIMIT $5`,
orderBy: orderByClause(by, me, models, type)
orderBy: (type === 'bookmarks' && by === 'custom')
? 'ORDER BY "Item"."bookmarkCustomOrder" ASC NULLS LAST, "Item"."bookmarkCreatedAt" DESC'
: orderByClause(by, me, models, type)
}, ...whenRange(when, from, to || decodedCursor.time), user.id, decodedCursor.offset, limit)
break
case 'recent':
Expand Down Expand Up @@ -772,6 +777,24 @@ export default {
} else await models.bookmark.create({ data })
return { id }
},
reorderBookmarks: async (parent, { itemIds }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
}

if (!itemIds || itemIds.length === 0) {
throw new GqlInputError('itemIds required')
}

await models.$transaction(
itemIds.map((id, i) => models.bookmark.update({
where: { userId_itemId: { userId: me.id, itemId: Number(id) } },
data: { customOrder: i + 1 }
}))
)

return true
},
pinItem: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GqlAuthenticationError()
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default gql`
extend type Mutation {
bookmarkItem(id: ID): Item
reorderBookmarks(itemIds: [ID!]!): Boolean
pinItem(id: ID): Item
subscribeItem(id: ID): Item
deleteItem(id: ID): Item
Expand Down
90 changes: 89 additions & 1 deletion components/bookmark.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { useMutation } from '@apollo/client'
import { useMutation, useQuery } from '@apollo/client'
import { gql } from 'graphql-tag'
import Dropdown from 'react-bootstrap/Dropdown'
import { useToast } from './toast'
import { useCallback, useMemo, useEffect, useState } from 'react'
import { DndProvider, useDndHandlers } from '@/wallets/client/context/dnd'
import { ListItem, ItemsSkeleton } from './items'
import MoreFooter from './more-footer'
import { useData } from './use-data'
import { SUB_ITEMS } from '@/fragments/subs'
import styles from './items.module.css'
import bookmarkStyles from '@/styles/bookmark.module.css'
import DragIcon from '@/svgs/draggable.svg'

export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
const toaster = useToast()
Expand Down Expand Up @@ -39,3 +48,82 @@ export default function BookmarkDropdownItem ({ item: { id, meBookmark } }) {
</Dropdown.Item>
)
}

const REORDER_BOOKMARKS = gql`
mutation reorderBookmarks($itemIds: [ID!]!) {
reorderBookmarks(itemIds: $itemIds)
}
`

function DraggableBookmarkItem ({ item, index, children, ...props }) {
const handlers = useDndHandlers(index)
return (
<div
draggable
onDragStart={handlers.handleDragStart}
onDragOver={handlers.handleDragOver}
onDragEnter={handlers.handleDragEnter}
onDragLeave={handlers.handleDragLeave}
onDrop={handlers.handleDrop}
onDragEnd={handlers.handleDragEnd}
onTouchStart={handlers.handleTouchStart}
onTouchMove={handlers.handleTouchMove}
onTouchEnd={handlers.handleTouchEnd}
data-index={index}
className={`${bookmarkStyles.draggableBookmark} ${handlers.isBeingDragged ? bookmarkStyles.dragging : ''} ${handlers.isDragOver ? bookmarkStyles.dragOver : ''}`}
{...props}
>
<DragIcon className={bookmarkStyles.dragHandle} width={14} height={14} />
{children}
</div>
)
}

export function CustomBookmarkList ({ ssrData, variables = {}, query }) {
const toaster = useToast()
const [reorderBookmarks] = useMutation(REORDER_BOOKMARKS)

const { data, fetchMore } = useQuery(query || SUB_ITEMS, { variables })
const dat = useData(data, ssrData)

const { items, cursor } = useMemo(() => dat?.items ?? { items: [], cursor: null }, [dat])

const [orderedItems, setOrderedItems] = useState(items || [])
useEffect(() => { setOrderedItems(items || []) }, [items])

const Skeleton = useCallback(() =>
<ItemsSkeleton startRank={items?.length} limit={variables.limit} Footer={MoreFooter} />, [items])

if (!dat) return <Skeleton />

const handleReorder = useCallback(async (newItems) => {
try {
const itemIds = newItems.map(item => item.id.toString())
await reorderBookmarks({ variables: { itemIds } })
toaster.success('bookmarks reordered')
} catch (err) {
console.error(err)
toaster.danger('failed to reorder bookmarks')
setOrderedItems(items || [])
}
}, [reorderBookmarks, toaster])

const visibleItems = useMemo(() => (orderedItems || []).filter(item => item?.meBookmark === true), [orderedItems])

return (
<DndProvider items={visibleItems} onReorder={(newItems) => { setOrderedItems(newItems); handleReorder(newItems) }}>
<div className={styles.grid}>
{visibleItems.map((item, i) => (
<DraggableBookmarkItem key={item.id} item={item} index={i}>
<ListItem item={item} itemClassName={variables.includeComments ? 'py-2' : ''} pinnable={false} />
</DraggableBookmarkItem>
))}
</div>
<MoreFooter
cursor={cursor} fetchMore={fetchMore}
count={visibleItems.length}
Skeleton={Skeleton}
/>
</DndProvider>
)
}
2 changes: 1 addition & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const ITEM_FILTER_THRESHOLD = 1.2
export const DONT_LIKE_THIS_COST = 1
export const COMMENT_TYPE_QUERY = ['comments', 'freebies', 'outlawed', 'borderland', 'all', 'bookmarks']
export const USER_SORTS = ['value', 'stacking', 'spending', 'comments', 'posts', 'referrals']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats', 'boost']
export const ITEM_SORTS = ['zaprank', 'comments', 'sats', 'boost', 'custom']
export const SUB_SORTS = ['stacking', 'revenue', 'spending', 'posts', 'comments']
export const WHENS = ['day', 'week', 'month', 'year', 'forever', 'custom']
export const ITEM_TYPES_USER = ['all', 'posts', 'comments', 'bounties', 'links', 'discussions', 'polls', 'freebies', 'jobs', 'bookmarks']
Expand Down
25 changes: 20 additions & 5 deletions pages/[name]/[type].js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getGetServerSideProps } from '@/api/ssrApollo'
import Items from '@/components/items'
import { CustomBookmarkList } from '@/components/bookmark'
import { useRouter } from 'next/router'
import { USER, USER_WITH_ITEMS } from '@/fragments/users'
import { useQuery } from '@apollo/client'
Expand Down Expand Up @@ -31,11 +32,20 @@ export default function UserItems ({ ssrData }) {
<UserLayout user={user}>
<div className='mt-2'>
<UserItemsHeader type={variables.type} name={user.name} />
<Items
ssrData={ssrData}
variables={variables}
query={USER_WITH_ITEMS}
/>
{variables.type === 'bookmarks' && variables.by === 'custom'
? (
<CustomBookmarkList
ssrData={ssrData}
variables={variables}
query={USER_WITH_ITEMS}
/>
)
: (
<Items
ssrData={ssrData}
variables={variables}
query={USER_WITH_ITEMS}
/>)}
</div>
</UserLayout>
)
Expand Down Expand Up @@ -110,6 +120,11 @@ function UserItemsHeader ({ type, name }) {
when={when}
/>}
</div>
{type === 'bookmarks' && by === 'custom' && (
<div className='text-muted small mb-2'>
Drag and drop bookmarks to reorder them.
</div>
)}
</Form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Bookmark" ADD COLUMN "custom_order" INTEGER;

-- CreateIndex
CREATE INDEX "Bookmark.custom_order_index" ON "Bookmark"("custom_order");
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1045,11 +1045,13 @@ model Bookmark {
userId Int
itemId Int
createdAt DateTime @default(now()) @map("created_at")
customOrder Int? @map("custom_order")
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@id([userId, itemId])
@@index([createdAt], map: "Bookmark.created_at_index")
@@index([customOrder], map: "Bookmark.custom_order_index")
}

// TODO: make thread subscriptions for OP by default so they can
Expand Down
32 changes: 32 additions & 0 deletions styles/bookmark.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.draggableBookmark {
transition: all 0.2s ease;
cursor: grab;
position: relative;
grid-column: 1 / span 2;
}

.draggableBookmark:active,
.dragHandle:active {
cursor: grabbing;
}

.draggableBookmark.dragging {
opacity: 0.7;
transform: rotate(1deg);
}

.draggableBookmark.dragOver {
transform: translateY(-4px);
}

.dragHandle {
position: absolute;
top: 8px;
right: 8px;
opacity: 0.5;
cursor: grab;
}

.dragHandle:hover {
opacity: 0.6;
}