feat: add web bookmark creation (NIP-B0, kind:39701)

- Created webBookmarkService for creating web bookmarks
- Added AddBookmarkModal component with URL, title, and description fields
- Added plus button to sidebar header (visible when logged in)
- Modal validates URL format and publishes to relays
- Auto-refreshes bookmarks after creation
- Styled modal with dark theme matching app design
- Follows NIP-B0 spec: URL in 'd' tag, title and summary tags
This commit is contained in:
Gigi
2025-10-08 09:44:45 +01:00
parent 815b3cc57d
commit dcf43cfce1
4 changed files with 378 additions and 1 deletions

View File

@@ -0,0 +1,131 @@
import React, { useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import IconButton from './IconButton'
interface AddBookmarkModalProps {
onClose: () => void
onSave: (url: string, title?: string, description?: string) => Promise<void>
}
const AddBookmarkModal: React.FC<AddBookmarkModalProps> = ({ onClose, onSave }) => {
const [url, setUrl] = useState('')
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError(null)
if (!url.trim()) {
setError('URL is required')
return
}
// Validate URL
try {
new URL(url)
} catch {
setError('Please enter a valid URL')
return
}
try {
setIsSaving(true)
await onSave(
url.trim(),
title.trim() || undefined,
description.trim() || undefined
)
onClose()
} catch (err) {
console.error('Failed to save bookmark:', err)
setError(err instanceof Error ? err.message : 'Failed to save bookmark')
} finally {
setIsSaving(false)
}
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Add Bookmark</h2>
<IconButton
icon={faTimes}
onClick={onClose}
title="Close"
ariaLabel="Close modal"
variant="ghost"
/>
</div>
<form onSubmit={handleSubmit} className="modal-form">
<div className="form-group">
<label htmlFor="bookmark-url">URL *</label>
<input
id="bookmark-url"
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://example.com"
disabled={isSaving}
autoFocus
/>
</div>
<div className="form-group">
<label htmlFor="bookmark-title">Title</label>
<input
id="bookmark-title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Optional title"
disabled={isSaving}
/>
</div>
<div className="form-group">
<label htmlFor="bookmark-description">Description</label>
<textarea
id="bookmark-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description"
disabled={isSaving}
rows={3}
/>
</div>
{error && (
<div className="modal-error">{error}</div>
)}
<div className="modal-actions">
<button
type="button"
onClick={onClose}
className="btn-secondary"
disabled={isSaving}
>
Cancel
</button>
<button
type="submit"
className="btn-primary"
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Bookmark'}
</button>
</div>
</form>
</div>
</div>
)
}
export default AddBookmarkModal

View File

@@ -1,12 +1,15 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faRotate, faHome } from '@fortawesome/free-solid-svg-icons'
import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faRotate, faHome, faPlus } from '@fortawesome/free-solid-svg-icons'
import { Hooks } from 'applesauce-react'
import { useEventModel } from 'applesauce-react/hooks'
import { Models } from 'applesauce-core'
import { Accounts } from 'applesauce-accounts'
import IconButton from './IconButton'
import AddBookmarkModal from './AddBookmarkModal'
import { createWebBookmark } from '../services/webBookmarkService'
import { RELAYS } from '../config/relays'
interface SidebarHeaderProps {
onToggleCollapse: () => void
@@ -18,9 +21,11 @@ interface SidebarHeaderProps {
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogout, onOpenSettings, onRefresh, isRefreshing }) => {
const [isConnecting, setIsConnecting] = useState(false)
const [showAddModal, setShowAddModal] = useState(false)
const navigate = useNavigate()
const activeAccount = Hooks.useActiveAccount()
const accountManager = Hooks.useAccountManager()
const relayPool = Hooks.useRelayPool()
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
const handleLogin = async () => {
@@ -49,6 +54,19 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`
}
const handleSaveBookmark = async (url: string, title?: string, description?: string) => {
if (!activeAccount || !relayPool) {
throw new Error('Please login to create bookmarks')
}
await createWebBookmark(url, title, description, activeAccount, relayPool, RELAYS)
// Refresh bookmarks after creating
if (onRefresh) {
onRefresh()
}
}
const profileImage = getProfileImage()
return (
@@ -70,6 +88,15 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
ariaLabel="Home"
variant="ghost"
/>
{activeAccount && (
<IconButton
icon={faPlus}
onClick={() => setShowAddModal(true)}
title="Add bookmark"
ariaLabel="Add bookmark"
variant="ghost"
/>
)}
{onRefresh && (
<IconButton
icon={faRotate}
@@ -119,6 +146,12 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
)}
</div>
</div>
{showAddModal && (
<AddBookmarkModal
onClose={() => setShowAddModal(false)}
onSave={handleSaveBookmark}
/>
)}
</>
)
}