Skip to content
Merged
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
9 changes: 7 additions & 2 deletions src/core/components/SearchButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"
type Props = {
onChange?: (searchTerm: string) => void
debounceTime?: number
className?: string
}

const SearchButton = ({ onChange, debounceTime = 500 }: Props) => {
const SearchButton = ({ onChange, debounceTime = 500, className }: Props) => {
const currentSearchTerm = ""
const handleSearch = (value) => {
if (onChange != undefined) {
Expand All @@ -15,7 +16,11 @@ const SearchButton = ({ onChange, debounceTime = 500 }: Props) => {
}
return (
<div>
<div className="flex flex-row mx-auto w-full max-w-md items-center justify-between relative text-base-400 focus-within:text-base-600 bg-base-50">
<div
className={`flex flex-row mx-auto w-full max-w-md items-center justify-between relative text-base-400 focus-within:text-base-600 bg-base-50 ${
className ?? ""
}`}
>
<MagnifyingGlassIcon className="w-5 h-5 absolute ml-3 pointer-events-none"></MagnifyingGlassIcon>

<DebouncedInput
Expand Down
143 changes: 112 additions & 31 deletions src/notes/components/NotesPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { useQuery, invalidateQuery } from "@blitzjs/rpc"
import { useMutation } from "@blitzjs/rpc"
import { useState } from "react"
import { useState, useMemo } from "react"
import listNotes from "../queries/listNotes"
import deleteNote from "../mutations/deleteNote"
import updateNote from "src/notes/mutations/updateNote"
import NoteEditor from "./NotesEditor"
import SearchButton from "src/core/components/SearchButton"

type SortOption = "updatedAt" | "createdAt" | "title"

function downloadMarkdown(filename: string, content: string) {
const blob = new Blob([content], { type: "text/markdown" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}

export const NotesPanel = ({ projectId }: { projectId: number }) => {
const [includeArchived, setIncludeArchived] = useState(false)
const [sortBy, setSortBy] = useState<SortOption>("updatedAt")
const [searchQuery, setSearchQuery] = useState("")
const [notes, { refetch, setQueryData }] = useQuery(
listNotes,
{ projectId, includeArchived },
Expand All @@ -18,36 +33,95 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
const [creating, setCreating] = useState(false)
const [editingId, setEditingId] = useState<number | null>(null)

const sortedNotes = notes
? [...notes].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
: []
const filteredAndSortedNotes = useMemo(() => {
if (!notes) return []
let result = [...notes]

if (searchQuery.trim()) {
const q = searchQuery.toLowerCase()
result = result.filter((n) => (n.title ?? "Untitled").toLowerCase().includes(q))
}

result.sort((a, b) => {
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1
if (sortBy === "title") {
if (!a.title && b.title) return 1
if (a.title && !b.title) return -1
return (a.title ?? "").toLowerCase().localeCompare((b.title ?? "").toLowerCase())
}
if (sortBy === "createdAt") {
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
}
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
})

return result
}, [notes, searchQuery, sortBy])

const canEditRow = (n: any) => {
if (!n) return false
const ownerEditable = !!n.editable
const pmOverride = !!n.canSetContributors && n.visibility === "CONTRIBUTORS"
return ownerEditable || pmOverride
return !!n.editable || (!!n.canSetContributors && n.visibility === "CONTRIBUTORS")
}

const formatNoteMd = (n: any) => {
const title = n.title || "Untitled"
const created = new Date(n.createdAt).toLocaleString()
const edited = new Date(n.updatedAt).toLocaleString()
const meta = `_Created: ${created} · Last edited: ${edited}_`
const body = n.contentMarkdown ?? "(No content)"
return `# ${title}\n\n${meta}\n\n${body}`
}

const handleDownloadNote = (n: any) => {
const title = n.title || "Untitled"
const safeTitle = title.replace(/[^a-zA-Z0-9_\- ]/g, "").trim() || "note"
downloadMarkdown(`${safeTitle}.md`, formatNoteMd(n))
}

const handleDownloadAll = () => {
if (!filteredAndSortedNotes.length) return
const combined = filteredAndSortedNotes.map(formatNoteMd).join("\n\n---\n\n")
downloadMarkdown("notes.md", combined)
}

return (
<div className="space-y-4">
{/* Index toolbar — hidden while editing/creating */}
{!(creating || editingId !== null) && (
<div className="flex items-center justify-end">
<div className="flex items-center gap-2">
<label className="label cursor-pointer">
<span className="mr-2">Show archived</span>
<input
type="checkbox"
className="toggle toggle-sm"
checked={includeArchived}
onChange={(e) => setIncludeArchived(e.target.checked)}
/>
</label>
<button className="btn btn-primary" onClick={() => setCreating(true)}>
New note
</button>
<div className="flex items-center gap-2">
<div className="flex-1">
<SearchButton onChange={(val) => setSearchQuery(String(val))} className="max-w-none" />
</div>
<select
className="select rounded-full border-2 border-primary bg-base-300"
value={sortBy}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setSortBy(e.target.value as SortOption)
}
>
<option value="updatedAt">Last edited</option>
<option value="createdAt">Date created</option>
<option value="title">Title A–Z</option>
</select>
<label className="label cursor-pointer gap-2">
<span className="text-sm">Show archived</span>
<input
type="checkbox"
className="toggle toggle-sm"
checked={includeArchived}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIncludeArchived(e.target.checked)
}
/>
</label>
{filteredAndSortedNotes.length > 0 && (
<button className="btn btn-accent" onClick={handleDownloadAll}>
Download all
</button>
)}
<button className="btn btn-primary" onClick={() => setCreating(true)}>
New note
</button>
</div>
)}

Expand All @@ -57,7 +131,6 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
projectId={projectId}
className="shadow"
onCreated={async (id) => {
// First create → switch to edit mode so the editor stays open
setCreating(false)
setEditingId(id)
await refetch()
Expand Down Expand Up @@ -97,7 +170,7 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
{/* Index list — only when not editing/creating */}
{!(creating || editingId !== null) && (
<ul className="space-y-3">
{sortedNotes.map((n) => (
{filteredAndSortedNotes.map((n) => (
<li key={n.id} className="card bg-base-300 shadow border border-base-300">
<div className="card-body p-4">
<div className="flex items-center justify-between gap-2">
Expand Down Expand Up @@ -145,7 +218,7 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
</button>
{!n.archived && (
<button
className="btn btn-outline"
className="btn btn-warning"
disabled={!canEditRow(n)}
onClick={async () => {
await updateNoteMutation({ id: n.id, archived: true })
Expand All @@ -157,11 +230,10 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
)}
{n.archived && (
<button
className="btn btn-outline"
className="btn btn-warning"
disabled={!canEditRow(n)}
onClick={async () => {
await updateNoteMutation({ id: n.id, archived: false })
// Optimistically update local cache to reflect unarchive
await setQueryData((prev) =>
(prev ?? []).map((x) => (x.id === n.id ? { ...x, archived: false } : x))
)
Expand All @@ -172,6 +244,9 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
Unarchive
</button>
)}
<button className="btn btn-accent" onClick={() => handleDownloadNote(n)}>
Download
</button>
<button className="btn btn-primary" onClick={() => setEditingId(n.id)}>
{canEditRow(n) ? "Edit" : "View"}
</button>
Expand All @@ -198,13 +273,19 @@ export const NotesPanel = ({ projectId }: { projectId: number }) => {
</div>
</li>
))}
{!notes?.length && !creating && (
{!filteredAndSortedNotes.length && !creating && (
<div className="card bg-base-300 shadow border border-dashed border-base-300">
<div className="card-body items-center text-center p-6">
<div className="text-lg opacity-70 mb-3">No notes yet</div>
<button className="btn btn-primary" onClick={() => setCreating(true)}>
Create your first note
</button>
{notes?.length && searchQuery ? (
<div className="text-lg opacity-70">No notes match your search</div>
) : (
<>
<div className="text-lg opacity-70 mb-3">No notes yet</div>
<button className="btn btn-primary" onClick={() => setCreating(true)}>
Create your first note
</button>
</>
)}
</div>
</div>
)}
Expand Down
5 changes: 5 additions & 0 deletions src/pages/forms/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ const AllFormsPage = () => {
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
}

const handleFolderSelect = (id: number | null | "all") => {
setSelectedFolderId(id)
setPagination((p) => ({ ...p, pageIndex: 0 }))
}

const handleFolderFilterChange = useCallback((folderId: number | null | "all") => {
setSelectedFolderId(folderId)
setPagination((prev) => ({ ...prev, pageIndex: 0 }))
Expand Down
Loading