Skip to content

Commit 8a5d1ee

Browse files
committed
new feature to pin favorites
1 parent 0d4603e commit 8a5d1ee

8 files changed

+82
-36
lines changed

package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"iconoir-react": "^6.8.0",
1818
"react": "^18.2.0",
1919
"react-dom": "^18.2.0",
20+
"react-wrap-balancer": "^1.0.0",
2021
"sonner": "^0.4.0",
2122
"tailwindcss-animated": "^1.0.1",
2223
"zustand": "^4.3.8"

src/components/Bookmark.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { SyntheticEvent } from "react"
22
import { useBookmarkStore } from "../stores/BookmarkStore"
3+
import { Pin } from "iconoir-react"
34
import fallbackImage from "../assets/fallback.png"
45
import BookmarkTags from "./BookmarkTags"
56
import BookmarkDropdown from "./BookmarkDropdown"
@@ -21,8 +22,9 @@ const Bookmark = ({ bookmark }: Props) => {
2122

2223
return (
2324
<CardSpotlight>
24-
<div className="z-10 overflow-hidden rounded-md aspect-video bg-zinc-800">
25-
<img className="object-cover w-full h-full m-auto" src={bookmark.image ? bookmark.image : fallbackImage} alt={bookmark.title} onError={addImageFallback} />
25+
<div className="z-10 relative overflow-hidden rounded-md aspect-video bg-zinc-800">
26+
<img className="object-cover w-full h-full m-auto" src={bookmark.image ? bookmark.image : fallbackImage} alt={bookmark.title} loading="lazy" onError={addImageFallback} />
27+
{ bookmark.pinned && <PinBadge /> }
2628
</div>
2729
<div>
2830
<div className="flex items-center gap-2">
@@ -38,4 +40,12 @@ const Bookmark = ({ bookmark }: Props) => {
3840
)
3941
}
4042

43+
const PinBadge = () => {
44+
return (
45+
<div className="absolute top-2 right-2 w-6 h-6 flex items-center justify-center rounded-full bg-zinc-800">
46+
<Pin width={16} />
47+
</div>
48+
)
49+
}
50+
4151
export default Bookmark

src/components/BookmarkDropdown.tsx

+44-24
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
import { Fragment, useState } from "react"
1+
import { Fragment, useState, ReactNode } from "react"
22
import { Menu, Transition } from "@headlessui/react"
3-
import { OpenInBrowser, Trash, Copy } from "iconoir-react"
3+
import { OpenInBrowser, Trash, Copy, Pin } from "iconoir-react"
44
import { toast } from "sonner"
5-
import DeleteModal from "./DeleteModal"
5+
import { useBookmarkStore } from "../stores/BookmarkStore"
6+
import { useAuthStore } from "../stores/AuthStore"
67
import useClipboard from "../hooks/useClipboard"
8+
import DeleteModal from "./DeleteModal"
79

810
type Props = {
911
bookmark: Bookmark
1012
}
1113

1214
const BookmarkDropdown = ({ bookmark }: Props) => {
15+
const { fetch: getBookmarks, update: updateBookmark } = useBookmarkStore(state => ({ fetch: state.fetch, update: state.update }))
1316
const { copy } = useClipboard()
17+
const session = useAuthStore(state => state.session)
18+
const userId = session?.user.id
1419
const [ isModalOpen, setIsModalOpen ] = useState<boolean>(false)
1520

1621
const openInNewTab = (url: string) => window.open(url, "_blank")
1722

23+
const pinBookmark = async () => {
24+
if (!userId) return
25+
const response = await updateBookmark(bookmark.id, { ...bookmark, pinned: !bookmark.pinned })
26+
if (!response.success) return toast.error(response.data)
27+
getBookmarks(userId)
28+
}
29+
1830
const copyUrl = (url: string) => {
1931
copy(url)
2032
toast("URL copied to clipboard!", {style: { backgroundColor: "#18181b", borderColor: "#3f3f46" }})
@@ -30,27 +42,18 @@ const BookmarkDropdown = ({ bookmark }: Props) => {
3042
</Menu.Button>
3143
<Transition as={Fragment} enter="transition ease-out duration-100" enterFrom="transform opacity-0 scale-95" enterTo="transform opacity-100 scale-100" leave="transition ease-in duration-100" leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95">
3244
<Menu.Items className="absolute p-[1px] right-0 w-40 mt-2 origin-top-right bg-zinc-200 rounded-md text-sm z-20 shadow-xl focus:outline-non will-change-transform">
33-
<Menu.Item>
34-
{({ active }) => (
35-
<button onClick={() => openInNewTab(bookmark.url)} className={`${active ? "bg-zinc-800 text-zinc-200" : "text-zinc-900"} flex gap-1 w-full items-center rounded-[5px] p-2`}>
36-
<OpenInBrowser width={16} strokeWidth={1.75} />Open in new tab
37-
</button>
38-
)}
39-
</Menu.Item>
40-
<Menu.Item>
41-
{({ active }) => (
42-
<button onClick={() => copyUrl(bookmark.url)} className={`${active ? "bg-zinc-800 text-zinc-200" : "text-zinc-900"} flex gap-1 w-full items-center rounded-[5px] p-2`}>
43-
<Copy width={16} strokeWidth={1.75} />Copy URL
44-
</button>
45-
)}
46-
</Menu.Item>
47-
<Menu.Item>
48-
{({ active }) => (
49-
<button onClick={() => setIsModalOpen(true)} className={`${active ? "bg-zinc-800 text-zinc-200" : "text-zinc-900"} flex gap-1 w-full items-center rounded-[5px] p-2`}>
50-
<Trash width={16} strokeWidth={1.75} />Delete
51-
</button>
52-
)}
53-
</Menu.Item>
45+
<MenuItem onClick={() => openInNewTab(bookmark.url)}>
46+
<OpenInBrowser width={16} strokeWidth={1.75} />Open in new tab
47+
</MenuItem>
48+
<MenuItem onClick={() => copyUrl(bookmark.url)}>
49+
<Copy width={16} strokeWidth={1.75} />Copy URL
50+
</MenuItem>
51+
<MenuItem onClick={pinBookmark}>
52+
<Pin width={16} strokeWidth={1.75} />{ bookmark.pinned ? "Unpin" : "Pin to top" }
53+
</MenuItem>
54+
<MenuItem onClick={() => setIsModalOpen(true)}>
55+
<Trash width={16} strokeWidth={1.75} />Delete
56+
</MenuItem>
5457
</Menu.Items>
5558
</Transition>
5659
</Menu>
@@ -59,4 +62,21 @@ const BookmarkDropdown = ({ bookmark }: Props) => {
5962
)
6063
}
6164

65+
type MenuItemProps = {
66+
onClick: React.MouseEventHandler<HTMLButtonElement>
67+
children: ReactNode
68+
}
69+
70+
const MenuItem = ({ onClick, children }: MenuItemProps) => {
71+
return (
72+
<Menu.Item>
73+
{({ active }) => (
74+
<button onClick={onClick} className={`${active ? "bg-zinc-800 text-zinc-200" : "text-zinc-900"} flex gap-1 w-full items-center rounded-[5px] p-2`}>
75+
{children}
76+
</button>
77+
)}
78+
</Menu.Item>
79+
)
80+
}
81+
6282
export default BookmarkDropdown

src/components/BookmarkTags.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ const BookmarkTags = ({ bookmark }: Props) => {
1919
const formRef = useRef<HTMLFormElement>(null)
2020
useClickOutside(formRef, () => setEditable(false))
2121

22-
const handleUpdate = async (e: FormEvent<HTMLFormElement>) => {
22+
const updateTags = async (e: FormEvent<HTMLFormElement>) => {
2323
e.preventDefault()
2424
if (!userId) return
2525
const tagList: string[] = newTags.replace(/\s/g, "").toLowerCase().split(",").filter(tag => tag)
26-
const response = await updateBookmark(bookmark.id, tagList)
26+
const response = await updateBookmark(bookmark.id, { ...bookmark, tags: tagList })
2727
if (!response.success) return toast.error(response.data)
2828
setEditable(false)
2929
}
@@ -32,10 +32,10 @@ const BookmarkTags = ({ bookmark }: Props) => {
3232
<div className="flex flex-wrap gap-2 mb-2 font-mono text-xs" onClick={() => setEditable(true)}>
3333
{ !bookmark.tags.length && !editable && <p className="py-1 border-b border-transparent text-zinc-500">add tags...</p> }
3434
{ bookmark && editable
35-
? <form onSubmit={handleUpdate} ref={formRef} className="flex w-full gap-2">
35+
? <form onSubmit={updateTags} ref={formRef} className="flex w-full gap-2">
3636
<input type="text" value={newTags} onChange={(e) => setNewTags(e.target.value)} autoFocus className="max-w-full py-1 bg-transparent border-b border-zinc-600 focus:outline-none" style={{ width: `${newTags.length + 1}ch` }} />
3737
<button type="submit">
38-
<Check className="transition-all cursor-pointer text-zinc-400 hover:text-zinc-200" width={16} strokeWidth={1.75} />
38+
<Check className="transition-all cursor-pointer text-zinc-400 hover:text-zinc-200" width={16} />
3939
</button>
4040
</form>
4141
: bookmark.tags.map(tag => <span key={tag} className="px-2 py-1 truncate border-b border-transparent rounded-md bg-zinc-700/50 text-zinc-400">{tag}</span> )

src/components/Headline.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import Balancer from "react-wrap-balancer"
2+
13
const Headline = () => {
24
return (
35
<div className="flex flex-col gap-6 mt-20 mb-8 text-center text-transparent from-zinc-400 via-zinc-200 to-zinc-400 bg-gradient-to-r bg-clip-text animate-fade-down animate-duration-500">
4-
<h1 className="max-w-md text-5xl font-bold md:max-w-2xl md:text-7xl">Say goodbye to messy bookmarks</h1>
6+
<Balancer>
7+
<h1 className="sm:text-6xl text-5xl font-bold md:text-7xl max-w-5xl">Say goodbye to messy bookmarks</h1>
8+
</Balancer>
59
<p className="text-lg break-words">Organize your online life effortlessly!</p>
610
</div>
711
)

src/stores/BookmarkStore.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type BookmarkState = {
99
fetch: (userId: string) => Promise<StoreResponse>
1010
add: (url: string, savedBy: string) => Promise<StoreResponse>
1111
delete: (bookmarkId: number) => Promise<StoreResponse>
12-
update: (bookmarkId: number, tags: string[]) => Promise<StoreResponse>
12+
update: (bookmarkId: number, updatedBookmark: Bookmark) => Promise<StoreResponse>
1313
selectedTag: string
1414
setSelectedTag: (tag: string) => void
1515
}
@@ -24,7 +24,8 @@ export const useBookmarkStore = create<BookmarkState>(set => ({
2424
.from("bookmarks")
2525
.select("*")
2626
.eq("saved_by", userId)
27-
.order("created_at", { ascending: false } )
27+
.order("pinned", { ascending: false })
28+
.order("created_at", { ascending: false })
2829
.returns<Bookmark[]>()
2930
if (error) throw new Error(`Error fetching bookmarks: ${error.message}`)
3031
set({ bookmarks: data })
@@ -77,17 +78,17 @@ export const useBookmarkStore = create<BookmarkState>(set => ({
7778
set({ loading: false })
7879
}
7980
},
80-
update: async (bookmarkId, tags) => {
81+
update: async (bookmarkId, updatedBookmark) => {
8182
try {
8283
const { data, error } = await supabase
8384
.from("bookmarks")
84-
.update({ tags: tags })
85+
.update(updatedBookmark)
8586
.eq("id", bookmarkId)
8687
.select()
8788
.returns<Bookmark[]>()
8889
if (error) throw new Error(`Error updating bookmark: ${error.message}`)
8990
set((state) => ({
90-
bookmarks: state.bookmarks.map(bookmark => bookmark.id === bookmarkId ? { ...bookmark, tags: tags } : bookmark)
91+
bookmarks: state.bookmarks.map(bookmark => bookmark.id === bookmarkId ? updatedBookmark : bookmark)
9192
}))
9293
return { data, success: true }
9394
} catch (error) {

src/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ type Bookmark = {
88
created_at: string
99
saved_by: string
1010
tags: string[]
11+
pinned: boolean
1112
}
1213

1314
type Metadata = {

0 commit comments

Comments
 (0)