@coji/journal-mcp
Version:
MCP server for journal entries with web viewer
1,118 lines (1,117 loc) • 41.3 kB
JavaScript
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
};