UNPKG

@coji/journal-mcp

Version:

MCP server for journal entries with web viewer

1,118 lines (1,117 loc) 41.3 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { PassThrough } from "node:stream"; import { createReadableStreamFromReadable } from "@react-router/node"; import { ServerRouter, useMatches, useActionData, useLoaderData, useParams, useRouteError, Meta, Links, ScrollRestoration, Scripts, Outlet, isRouteErrorResponse, useLocation, Link, NavLink, useSearchParams, Form } from "react-router"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server"; import { createElement, useState } from "react"; import matter from "gray-matter"; import glob from "fast-glob"; import { homedir } from "node:os"; import { join } from "node:path"; import { promises } from "node:fs"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc.js"; import timezone from "dayjs/plugin/timezone.js"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; const streamTimeout = 5e3; function handleRequest(request, responseStatusCode, responseHeaders, routerContext, loadContext) { return new Promise((resolve, reject) => { let shellRendered = false; let userAgent = request.headers.get("user-agent"); let readyOption = userAgent && isbot(userAgent) || routerContext.isSpaMode ? "onAllReady" : "onShellReady"; const { pipe, abort } = renderToPipeableStream( /* @__PURE__ */ jsx(ServerRouter, { context: routerContext, url: request.url }), { [readyOption]() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode }) ); pipe(body); }, onShellError(error) { reject(error); }, onError(error) { responseStatusCode = 500; if (shellRendered) { console.error(error); } } } ); setTimeout(abort, streamTimeout + 1e3); }); } const entryServer = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: handleRequest, streamTimeout }, Symbol.toStringTag, { value: "Module" })); function withComponentProps(Component) { return function Wrapped() { const props = { params: useParams(), loaderData: useLoaderData(), actionData: useActionData(), matches: useMatches() }; return createElement(Component, props); }; } function withErrorBoundaryProps(ErrorBoundary3) { return function Wrapped() { const props = { params: useParams(), loaderData: useLoaderData(), actionData: useActionData(), error: useRouteError() }; return createElement(ErrorBoundary3, props); }; } const links = () => [{ rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" }]; function Layout({ children }) { return /* @__PURE__ */ jsxs("html", { lang: "en", children: [/* @__PURE__ */ jsxs("head", { children: [/* @__PURE__ */ jsx("meta", { charSet: "utf-8" }), /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), /* @__PURE__ */ jsx(Meta, {}), /* @__PURE__ */ jsx(Links, {})] }), /* @__PURE__ */ jsxs("body", { children: [children, /* @__PURE__ */ jsx(ScrollRestoration, {}), /* @__PURE__ */ jsx(Scripts, {})] })] }); } const root = withComponentProps(function App() { return /* @__PURE__ */ jsx(Outlet, {}); }); const ErrorBoundary = withErrorBoundaryProps(function ErrorBoundary2({ error }) { let message = "Oops!"; let details = "An unexpected error occurred."; let stack; if (isRouteErrorResponse(error)) { message = error.status === 404 ? "404" : "Error"; details = error.status === 404 ? "The requested page could not be found." : error.statusText || details; } return /* @__PURE__ */ jsxs("main", { className: "pt-16 p-4 container mx-auto", children: [/* @__PURE__ */ jsx("h1", { children: message }), /* @__PURE__ */ jsx("p", { children: details }), stack] }); }); const route0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, ErrorBoundary, Layout, default: root, links }, Symbol.toStringTag, { value: "Module" })); const route$4 = withComponentProps(function Layout2() { const location = useLocation(); const navigation = [{ name: "Home", href: "/", icon: "🏠" }, { name: "Search", href: "/search", icon: "🔍" }, { name: "Tags", href: "/tags", icon: "🏷️" }]; return /* @__PURE__ */ jsxs("div", { className: "min-h-screen bg-gray-50 grid grid-rows-[auto_1fr_auto]", children: [/* @__PURE__ */ jsx("nav", { className: "bg-white shadow-sm border-b border-gray-200", children: /* @__PURE__ */ jsx("div", { className: "max-w-4xl mx-auto px-4 sm:px-6 lg:px-8", children: /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center h-16", children: [/* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsx(Link, { to: "/", className: "text-xl font-bold text-gray-900", children: "📖 Journal" }) }), /* @__PURE__ */ jsx("div", { className: "flex space-x-1", children: navigation.map((item) => { location.pathname === item.href || item.href !== "/" && location.pathname.startsWith(item.href); return /* @__PURE__ */ jsxs(NavLink, { to: item.href, className: "px-3 py-2 rounded-md text-sm font-medium transition-colors aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-700 text-gray-500 hover:text-gray-700 hover:bg-gray-100", children: [/* @__PURE__ */ jsx("span", { className: "mr-1", children: item.icon }), item.name] }, item.name); }) })] }) }) }), /* @__PURE__ */ jsx("main", { className: "max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8", children: /* @__PURE__ */ jsx(Outlet, {}) }), /* @__PURE__ */ jsx("footer", { className: "bg-white border-t border-gray-200", children: /* @__PURE__ */ jsx("div", { className: "max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6", children: /* @__PURE__ */ jsx("p", { className: "text-center text-sm text-gray-500", children: "Journal MCP Server - Web Viewer" }) }) })] }); }); const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: route$4 }, Symbol.toStringTag, { value: "Module" })); function SingleEntry({ entry: entry2, date }) { return /* @__PURE__ */ jsxs("div", { className: "p-4 outline rounded-md", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-2", children: [ /* @__PURE__ */ jsx("span", { className: "text-sm font-mono text-gray-600", children: entry2.timestamp }), /* @__PURE__ */ jsx("h4", { className: "font-medium text-gray-900", children: entry2.title }) ] }), /* @__PURE__ */ jsx("div", { className: "prose prose-sm max-w-none text-gray-700 mb-3", children: entry2.content.split("\n").map((line, idx) => /* @__PURE__ */ jsx("p", { className: "mb-1", children: line }, idx)) }), entry2.tags.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: entry2.tags.map((tag) => /* @__PURE__ */ jsxs( Link, { to: `/search?tags=${encodeURIComponent(tag)}`, className: "text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors", children: [ "#", tag ] }, tag )) }) ] }); } function EntryCard({ file }) { return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [ /* @__PURE__ */ jsx( Link, { to: `/entries/${file.date}`, className: "text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors", children: file.date } ), /* @__PURE__ */ jsxs("div", { className: "text-sm text-gray-500", children: [ file.entries_count, " ", file.entries_count === 1 ? "entry" : "entries" ] }) ] }), file.tags.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mb-4", children: file.tags.map((tag) => /* @__PURE__ */ jsxs( Link, { to: `/search?tags=${encodeURIComponent(tag)}`, className: "text-xs px-2 py-1 bg-gray-100 text-gray-600 rounded-full hover:bg-gray-200 transition-colors", children: [ "#", tag ] }, tag )) }), /* @__PURE__ */ jsx("div", { className: "space-y-4", children: file.entries.map((entry2, idx) => /* @__PURE__ */ jsx( SingleEntry, { entry: entry2, date: file.date }, `${entry2.id}_${idx}` )) }), /* @__PURE__ */ jsxs( "div", { className: "mt-4 pt-4 border-t border-gray-100 text-xs text-gray-500", suppressHydrationWarning: true, children: [ "Last updated: ", new Date(file.updated).toLocaleDateString() ] } ) ] }); } function getJournalDataDir() { const xdgDataHome = process.env.XDG_DATA_HOME; if (xdgDataHome) { return join(xdgDataHome, "journal-mcp"); } return join(homedir(), ".local", "share", "journal-mcp"); } function getEntriesDir() { return join(getJournalDataDir(), "entries"); } function getDateFilePath(date) { const dateObj = new Date(date); const year = dateObj.getFullYear().toString(); const month = (dateObj.getMonth() + 1).toString().padStart(2, "0"); const filename = `${date}.md`; return join(getEntriesDir(), year, month, filename); } function parseDateFromPath(filePath) { const match = filePath.match(/(\d{4}-\d{2}-\d{2})\.md$/); return match ? match[1] : null; } async function ensureDir(dirPath) { try { await promises.access(dirPath); } catch { await promises.mkdir(dirPath, { recursive: true }); } } async function readFileIfExists(filePath) { try { return await promises.readFile(filePath, "utf-8"); } catch { return null; } } dayjs.extend(utc); dayjs.extend(timezone); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; const now = () => dayjs().tz(tz); function parseEntriesFromMarkdown(content, date) { const entries = []; const lines = content.split("\n"); let currentEntry = null; let contentLines = []; for (const line of lines) { const timeMatch = line.match(/^## (\d{2}:\d{2})\s*-?\s*(.*)$/); if (timeMatch) { if (currentEntry) { currentEntry.content = contentLines.join("\n").trim(); entries.push(currentEntry); } const [, time, title] = timeMatch; const timestamp = `${date}T${time}:00`; currentEntry = { id: `${date}-${time.replace(":", "")}`, title: title.trim() || "Entry", timestamp: time, created: timestamp, updated: timestamp, tags: [], content: "" }; contentLines = []; } else if (currentEntry) { contentLines.push(line); const tagMatches = line.match(/#(\w+)/g); if (tagMatches) { const tags = tagMatches.map((tag) => tag.slice(1)); currentEntry.tags = [...currentEntry.tags || [], ...tags]; } } } if (currentEntry) { currentEntry.content = contentLines.join("\n").trim(); entries.push(currentEntry); } entries.forEach((entry2) => { entry2.tags = Array.from(new Set(entry2.tags)).sort(); }); return entries; } async function parseJournalFile(filePath, content) { const { data: frontmatter, content: body } = matter(content); const date = parseDateFromPath(filePath) || frontmatter.title || ""; const entries = parseEntriesFromMarkdown(body, date); return { title: frontmatter.title || date, tags: frontmatter.tags || [], created: frontmatter.created || now().toISOString(), updated: frontmatter.updated || now().toISOString(), entries_count: frontmatter.entries_count || entries.length, entries, filePath, date }; } function filterJournalFiles(files, options) { return files.filter((file) => { if (options.dateFrom && file.date < options.dateFrom) return false; if (options.dateTo && file.date > options.dateTo) return false; if (options.tags && options.tags.length > 0) { const hasRequiredTags = options.tags.every( (tag) => file.tags.includes(tag) ); if (!hasRequiredTags) return false; } if (options.keywords) { const keyword = options.keywords.toLowerCase(); const searchText = `${file.title} ${file.entries.map((e) => e.content).join(" ")}`.toLowerCase(); if (!searchText.includes(keyword)) return false; } return true; }); } async function searchEntries$1(options = {}) { const entriesDir = getEntriesDir(); await ensureDir(entriesDir); const pattern = `${entriesDir}/**/*.md`; const files = await glob(pattern, { onlyFiles: true }); let journalFiles = []; for (const filePath of files) { const content = await readFileIfExists(filePath); if (!content) continue; try { const journalFile = await parseJournalFile(filePath, content); journalFiles.push(journalFile); } catch (error) { console.warn(`Failed to parse journal file ${filePath}:`, error); } } journalFiles = filterJournalFiles(journalFiles, options); journalFiles.sort( (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() ); const offset = options.offset || 0; const limit = options.limit || 50; const total = journalFiles.length; const paginatedFiles = journalFiles.slice(offset, offset + limit); return { entries: paginatedFiles, total, hasMore: offset + limit < total }; } async function getRecentEntries$1(limit = 10) { const result = await searchEntries$1({ limit }); return result.entries; } async function getEntryByDate$1(date) { const filePath = getDateFilePath(date); const content = await readFileIfExists(filePath); if (!content) return null; try { return await parseJournalFile(filePath, content); } catch { return null; } } async function listTags$1() { const result = await searchEntries$1(); const tagCounts = /* @__PURE__ */ new Map(); for (const file of result.entries) { for (const tag of file.tags) { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); } } return Array.from(tagCounts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count); } async function getStats$1() { const result = await searchEntries$1(); const files = result.entries; if (files.length === 0) { return { totalEntries: 0, totalFiles: 0, dateRange: { earliest: "", latest: "" }, topTags: [] }; } const totalEntries = files.reduce((sum, file) => sum + file.entries_count, 0); const dates = files.map((f) => f.date).sort(); const topTags = await listTags$1(); return { totalEntries, totalFiles: files.length, dateRange: { earliest: dates[0], latest: dates[dates.length - 1] }, topTags: topTags.slice(0, 10) }; } const getRecentEntries = async (limit = 10) => { const entries = await getRecentEntries$1(limit); return entries.map((entry2) => ({ title: entry2.title, tags: entry2.tags, created: entry2.created, updated: entry2.updated, entries_count: entry2.entries.length, entries: entry2.entries, filePath: entry2.filePath, date: entry2.date })); }; const searchEntries = async (options = {}) => { const { dateFrom, dateTo, tags, keywords, limit = 10, offset = 0 } = options; const searchResults = await searchEntries$1({ dateFrom, dateTo, tags, keywords, limit, offset }); return { entries: searchResults.entries.map((entry2) => ({ title: entry2.title, tags: entry2.tags, created: entry2.created, updated: entry2.updated, entries_count: entry2.entries.length, entries: entry2.entries, filePath: entry2.filePath, date: entry2.date })), total: searchResults.total, hasMore: searchResults.hasMore }; }; const getEntryByDate = async (date) => { const entry2 = await getEntryByDate$1(date); if (entry2) { return { title: entry2.title, tags: entry2.tags, created: entry2.created, updated: entry2.updated, entries_count: entry2.entries.length, entries: entry2.entries, filePath: entry2.filePath, date: entry2.date }; } return null; }; const listTags = async () => { const tags = await listTags$1(); return tags.map((tag) => ({ tag: tag.tag, count: tag.count })); }; const getStats = async () => { const stats = await getStats$1(); if (stats) { return { totalEntries: stats.totalEntries, totalFiles: stats.totalFiles, dateRange: { earliest: stats.dateRange.earliest, latest: stats.dateRange.latest }, topTags: stats.topTags.map((tag) => ({ tag: tag.tag, count: tag.count })) }; } return { totalEntries: 0, totalFiles: 0, dateRange: { earliest: "", latest: "" }, topTags: [] }; }; function meta$3({}) { return [{ title: "Journal - Home" }, { name: "description", content: "Recent journal entries" }]; } async function loader$3() { const recentEntries = await getRecentEntries(10); const stats = await getStats(); return { recentEntries, stats }; } const route$3 = withComponentProps(function Home({ loaderData }) { const { recentEntries, stats } = loaderData; return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: "Recent Journal Entries" }), /* @__PURE__ */ jsxs("p", { className: "text-gray-600", children: [stats.totalEntries, " entries across ", stats.totalFiles, " days"] })] }), recentEntries.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center", children: [/* @__PURE__ */ jsx("div", { className: "text-6xl mb-4", children: "📝" }), /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold text-gray-900 mb-2", children: "No journal entries yet" }), /* @__PURE__ */ jsx("p", { className: "text-gray-600 mb-4", children: "Start writing your first journal entry using Claude Desktop with the journal MCP server." }), /* @__PURE__ */ jsxs("div", { className: "bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto", children: [/* @__PURE__ */ jsx("p", { className: "text-sm text-gray-700 font-medium mb-2", children: "Try asking Claude:" }), /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-600 italic", children: '"Add a journal entry about what I learned today"' })] })] }) : /* @__PURE__ */ jsx("div", { className: "space-y-6", children: recentEntries.map((file) => /* @__PURE__ */ jsx(EntryCard, { file }, file.date)) })] }); }); const route2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: route$3, loader: loader$3, meta: meta$3 }, Symbol.toStringTag, { value: "Module" })); function meta$2({ params }) { return [{ title: `Journal - ${params.date}` }, { name: "description", content: `Journal entry for ${params.date}` }]; } async function loader$2({ params }) { const entry2 = await getEntryByDate(params.date); if (!entry2) { throw new Response("Journal entry not found", { status: 404 }); } return { entry: entry2 }; } const route$2 = withComponentProps(function EntryByDate({ loaderData }) { const { entry: entry2 } = loaderData; return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: entry2.date }), /* @__PURE__ */ jsxs("p", { className: "text-gray-600", children: [entry2.entries_count, " ", entry2.entries_count === 1 ? "entry" : "entries"] })] }), /* @__PURE__ */ jsx(Link, { to: "/", className: "px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors", children: "← Back to Home" })] }), /* @__PURE__ */ jsx(EntryCard, { file: entry2 })] }); }); const route3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: route$2, loader: loader$2, meta: meta$2 }, Symbol.toStringTag, { value: "Module" })); function cn(...inputs) { return twMerge(clsx(inputs)); } function Input({ className, type, ...props }) { return /* @__PURE__ */ jsx( "input", { type, "data-slot": "input", className: cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className ), ...props } ); } function Label({ className, ...props }) { return /* @__PURE__ */ jsx( LabelPrimitive.Root, { "data-slot": "label", className: cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className ), ...props } ); } const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline" }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9" } }, defaultVariants: { variant: "default", size: "default" } } ); function Button({ className, variant, size, asChild = false, ...props }) { const Comp = asChild ? Slot : "button"; return /* @__PURE__ */ jsx( Comp, { "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props } ); } function SearchForm() { const [searchParams] = useSearchParams(); const [keywords, setKeywords] = useState(searchParams.get("keywords") || ""); const [tags, setTags] = useState(searchParams.get("tags") || ""); const [dateFrom, setDateFrom] = useState(searchParams.get("dateFrom") || ""); const [dateTo, setDateTo] = useState(searchParams.get("dateTo") || ""); return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-6", children: [ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Search Journal Entries" }), /* @__PURE__ */ jsxs(Form, { method: "get", className: "space-y-4", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx( Label, { htmlFor: "keywords", className: "block text-sm font-medium text-gray-700 mb-1", children: "Keywords" } ), /* @__PURE__ */ jsx( Input, { type: "text", id: "keywords", name: "keywords", value: keywords, onChange: (e) => setKeywords(e.target.value), placeholder: "Search in content...", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" } ) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx( Label, { htmlFor: "tags", className: "block text-sm font-medium text-gray-700 mb-1", children: "Tags" } ), /* @__PURE__ */ jsx( Input, { type: "text", id: "tags", name: "tags", value: tags, onChange: (e) => setTags(e.target.value), placeholder: "e.g., work, meeting, learning", className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" } ), /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: "Comma-separated tags" }) ] }), /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx( Label, { htmlFor: "dateFrom", className: "block text-sm font-medium text-gray-700 mb-1", children: "From Date" } ), /* @__PURE__ */ jsx( Input, { type: "date", id: "dateFrom", name: "dateFrom", value: dateFrom, onChange: (e) => setDateFrom(e.target.value), className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" } ) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx( Label, { htmlFor: "dateTo", className: "block text-sm font-medium text-gray-700 mb-1", children: "To Date" } ), /* @__PURE__ */ jsx( Input, { type: "date", id: "dateTo", name: "dateTo", value: dateTo, onChange: (e) => setDateTo(e.target.value), className: "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" } ) ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [ /* @__PURE__ */ jsx(Button, { type: "submit", children: "Search" }), /* @__PURE__ */ jsx( Button, { type: "button", onClick: () => { setKeywords(""); setTags(""); setDateFrom(""); setDateTo(""); }, variant: "ghost", children: "Clear" } ) ] }) ] }) ] }); } function meta$1({}) { return [{ title: "Journal - Search" }, { name: "description", content: "Search journal entries" }]; } async function loader$1({ request }) { var _a; const url = new URL(request.url); const searchParams = { keywords: url.searchParams.get("keywords") || void 0, tags: ((_a = url.searchParams.get("tags")) == null ? void 0 : _a.split(",").map((t) => t.trim()).filter(Boolean)) || void 0, dateFrom: url.searchParams.get("dateFrom") || void 0, dateTo: url.searchParams.get("dateTo") || void 0, limit: 20 }; const hasSearchParams = searchParams.keywords || searchParams.tags || searchParams.dateFrom || searchParams.dateTo; if (!hasSearchParams) { return { results: null, searchParams: {} }; } const results = await searchEntries(searchParams); return { results, searchParams }; } const route$1 = withComponentProps(function Search({ loaderData: { results } }) { return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: "Search Journal Entries" }), /* @__PURE__ */ jsx("p", { className: "text-gray-600", children: "Find entries by keywords, tags, or date range." })] }), /* @__PURE__ */ jsx(SearchForm, {}), results && /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [/* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold text-gray-900", children: "Search Results" }), /* @__PURE__ */ jsxs("p", { className: "text-gray-600", children: [results.total, " ", results.total === 1 ? "result" : "results", " found", results.hasMore && ` (showing first ${results.entries.length})`] })] }), results.entries.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center", children: [/* @__PURE__ */ jsx("div", { className: "text-4xl mb-4", children: "🔍" }), /* @__PURE__ */ jsx("h3", { className: "text-lg font-medium text-gray-900 mb-2", children: "No results found" }), /* @__PURE__ */ jsx("p", { className: "text-gray-600", children: "Try adjusting your search criteria or keywords." })] }) : /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [results.entries.map((file) => /* @__PURE__ */ jsx(EntryCard, { file }, file.date)), results.hasMore && /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsxs("p", { className: "text-gray-600", children: ["Showing ", results.entries.length, " of ", results.total, " results"] }) })] })] })] }); }); const route4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: route$1, loader: loader$1, meta: meta$1 }, Symbol.toStringTag, { value: "Module" })); function TagList({ tags }) { if (tags.length === 0) { return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-6", children: [ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: "Tags" }), /* @__PURE__ */ jsx("p", { className: "text-gray-500", children: "No tags found in journal entries." }) ] }); } return /* @__PURE__ */ jsxs("div", { className: "bg-white rounded-lg shadow-sm border border-gray-200 p-6", children: [ /* @__PURE__ */ jsxs("h2", { className: "text-lg font-semibold text-gray-900 mb-4", children: [ "Tags (", tags.length, ")" ] }), /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: tags.map(({ tag, count }) => /* @__PURE__ */ jsxs( Link, { to: `/search?tags=${encodeURIComponent(tag)}`, className: "inline-flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-700 rounded-full hover:bg-blue-100 transition-colors", children: [ /* @__PURE__ */ jsxs("span", { children: [ "#", tag ] }), /* @__PURE__ */ jsx("span", { className: "text-xs bg-blue-200 text-blue-800 px-2 py-0.5 rounded-full", children: count }) ] }, tag )) }) ] }); } function meta({}) { return [{ title: "Journal - Tags" }, { name: "description", content: "Browse journal entries by tags" }]; } async function loader() { const tags = await listTags(); return { tags }; } const route = withComponentProps(function Tags({ loaderData }) { const { tags } = loaderData; return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold text-gray-900 mb-2", children: "Tags" }), /* @__PURE__ */ jsx("p", { className: "text-gray-600", children: "Browse and search journal entries by tags." })] }), /* @__PURE__ */ jsx(TagList, { tags }), tags.length > 0 && /* @__PURE__ */ jsxs("div", { className: "bg-blue-50 rounded-lg p-4", children: [/* @__PURE__ */ jsx("h3", { className: "font-medium text-blue-900 mb-2", children: "💡 Tip" }), /* @__PURE__ */ jsx("p", { className: "text-blue-800 text-sm", children: "Click on any tag to search for entries containing that tag. You can also combine multiple tags in the search page." })] })] }); }); const route5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: route, loader, meta }, Symbol.toStringTag, { value: "Module" })); const serverManifest = { "entry": { "module": "/assets/entry.client-Bj675TY3.js", "imports": ["/assets/chunk-DQRVZFIR-BjeRaiJT.js", "/assets/index-2Yj8rXzq.js"], "css": [] }, "routes": { "root": { "id": "root", "parentId": void 0, "path": "", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": false, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": true, "module": "/assets/root-g70tSPBa.js", "imports": ["/assets/chunk-DQRVZFIR-BjeRaiJT.js", "/assets/index-2Yj8rXzq.js", "/assets/with-props-C5f5Zz1S.js"], "css": ["/assets/root-DQIX5Sh7.css"], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 }, "routes/_app+/_layout/route": { "id": "routes/_app+/_layout/route", "parentId": "root", "path": void 0, "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": false, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": false, "module": "/assets/route-CqaFymPa.js", "imports": ["/assets/with-props-C5f5Zz1S.js", "/assets/chunk-DQRVZFIR-BjeRaiJT.js"], "css": [], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 }, "routes/_app+/_index/route": { "id": "routes/_app+/_index/route", "parentId": "routes/_app+/_layout/route", "path": void 0, "index": true, "caseSensitive": void 0, "hasAction": false, "hasLoader": true, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": false, "module": "/assets/route-bDCvyn7d.js", "imports": ["/assets/with-props-C5f5Zz1S.js", "/assets/chunk-DQRVZFIR-BjeRaiJT.js", "/assets/entry-card-B52nTnJl.js"], "css": [], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 }, "routes/_app+/entries.$date/route": { "id": "routes/_app+/entries.$date/route", "parentId": "routes/_app+/_layout/route", "path": "entries/:date", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": false, "module": "/assets/route-nW6-cLfY.js", "imports": ["/assets/with-props-C5f5Zz1S.js", "/assets/chunk-DQRVZFIR-BjeRaiJT.js", "/assets/entry-card-B52nTnJl.js"], "css": [], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 }, "routes/_app+/search/route": { "id": "routes/_app+/search/route", "parentId": "routes/_app+/_layout/route", "path": "search", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": false, "module": "/assets/route-CfqqMXrs.js", "imports": ["/assets/with-props-C5f5Zz1S.js", "/assets/chunk-DQRVZFIR-BjeRaiJT.js", "/assets/index-2Yj8rXzq.js", "/assets/entry-card-B52nTnJl.js"], "css": [], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 }, "routes/_app+/tags/route": { "id": "routes/_app+/tags/route", "parentId": "routes/_app+/_layout/route", "path": "tags", "index": void 0, "caseSensitive": void 0, "hasAction": false, "hasLoader": true, "hasClientAction": false, "hasClientLoader": false, "hasClientMiddleware": false, "hasErrorBoundary": false, "module": "/assets/route-BjfaapWF.js", "imports": ["/assets/with-props-C5f5Zz1S.js", "/assets/chunk-DQRVZFIR-BjeRaiJT.js"], "css": [], "clientActionModule": void 0, "clientLoaderModule": void 0, "clientMiddlewareModule": void 0, "hydrateFallbackModule": void 0 } }, "url": "/assets/manifest-0ccd7b9d.js", "version": "0ccd7b9d", "sri": void 0 }; const assetsBuildDirectory = "build/client"; const basename = "/"; const future = { "unstable_middleware": false, "unstable_optimizeDeps": false, "unstable_splitRouteModules": false, "unstable_subResourceIntegrity": false, "unstable_viteEnvironmentApi": false }; const ssr = true; const isSpaMode = false; const prerender = []; const routeDiscovery = { "mode": "lazy", "manifestPath": "/__manifest" }; const publicPath = "/"; const entry = { module: entryServer }; const routes = { "root": { id: "root", parentId: void 0, path: "", index: void 0, caseSensitive: void 0, module: route0 }, "routes/_app+/_layout/route": { id: "routes/_app+/_layout/route", parentId: "root", path: void 0, index: void 0, caseSensitive: void 0, module: route1 }, "routes/_app+/_index/route": { id: "routes/_app+/_index/route", parentId: "routes/_app+/_layout/route", path: void 0, index: true, caseSensitive: void 0, module: route2 }, "routes/_app+/entries.$date/route": { id: "routes/_app+/entries.$date/route", parentId: "routes/_app+/_layout/route", path: "entries/:date", index: void 0, caseSensitive: void 0, module: route3 }, "routes/_app+/search/route": { id: "routes/_app+/search/route", parentId: "routes/_app+/_layout/route", path: "search", index: void 0, caseSensitive: void 0, module: route4 }, "routes/_app+/tags/route": { id: "routes/_app+/tags/route", parentId: "routes/_app+/_layout/route", path: "tags", index: void 0, caseSensitive: void 0, module: route5 } }; export { serverManifest as assets, assetsBuildDirectory, basename, entry, future, isSpaMode, prerender, publicPath, routeDiscovery, routes, ssr };