search bar

This commit is contained in:
Michael Zhang 2024-05-25 06:45:23 -05:00
parent d0d64cf018
commit e3c477181d
13 changed files with 2234 additions and 55 deletions

View file

@ -9,11 +9,20 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@floating-ui/react": "^0.26.16",
"@fontsource/inter": "^5.0.18", "@fontsource/inter": "^5.0.18",
"@mui/icons-material": "^5.15.18",
"@mui/material": "^5.15.18",
"@tanstack/react-query": "^5.37.1", "@tanstack/react-query": "^5.37.1",
"@tauri-apps/api": "^1", "@tauri-apps/api": "^1",
"@uiw/react-md-editor": "^4.0.4",
"javascript-time-ago": "^2.5.10",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-time-ago": "^7.3.3"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1", "@tauri-apps/cli": "^1",

View file

@ -6,9 +6,13 @@ import "./global.scss";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import NodeDisplay from "./components/NodeDisplay"; import NodeDisplay from "./components/NodeDisplay";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
TimeAgo.addDefaultLocale(en);
function App() { function App() {
const [nodesOpened, setNodesOpened] = useState<string[]>(() => []); const [nodesOpened, setNodesOpened] = useState<string[]>(() => []);

View file

@ -1,10 +1,11 @@
import styles from "./Header.module.scss"; import styles from "./Header.module.scss";
import SearchBar from "./SearchBar";
export default function Header() { export default function Header() {
return ( return (
<div className={styles.Header}> <div className={styles.Header}>
<span>Panorama</span> <span>Panorama</span>
<input type="text" placeholder="Search..." /> <SearchBar />
</div> </div>
); );
} }

View file

@ -1,18 +1,37 @@
.container { .container {
width: 400px; width: 500px;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow-y: auto; overflow-y: auto;
border: 1px solid lightgray; border: 1px solid lightgray;
border-radius: 4px; border-radius: 4px;
display: flex;
flex-direction: column;
}
.mdContent {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.mdEditor {
flex-grow: 1;
} }
.header { .header {
padding: 2px 12px; padding: 2px 12px;
background-color: lightgray; color: rgb(106, 103, 160);
color: gray; background: rgb(204, 201, 255);
background: linear-gradient(90deg, rgba(204, 201, 255, 1) 0%, rgba(255, 255, 255, 1) 100%);
} }
.body { .body {
flex-grow: 1;
padding: 12px; padding: 12px;
display: flex;
flex-direction: column;
} }

View file

@ -1,5 +1,9 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import styles from "./NodeDisplay.module.scss"; import styles from "./NodeDisplay.module.scss";
import ReactTimeAgo from "react-time-ago";
import Markdown from "react-markdown";
import MDEditor, { commands } from "@uiw/react-md-editor";
import { useState } from "react";
export interface NodeDisplayProps { export interface NodeDisplayProps {
id: string; id: string;
@ -19,7 +23,11 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<small>ID {id}</small> {isSuccess ? (
<NodeDisplayHeaderLoaded id={id} data={data} />
) : (
<small>ID {id}</small>
)}
</div> </div>
<div className={styles.body}> <div className={styles.body}>
{isSuccess ? ( {isSuccess ? (
@ -32,11 +40,51 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
); );
} }
function NodeDisplayHeaderLoaded({ id, data }) {
return (
<small>
Type {data.type} &middot; Last updated{" "}
<ReactTimeAgo date={data.created_at * 1000} /> &middot; {id}
</small>
);
}
function NodeDisplayLoaded({ id, data }) { function NodeDisplayLoaded({ id, data }) {
const [value, setValue] = useState(() => data.content);
const [isEditing, setIsEditing] = useState(() => false);
return ( return (
<> <>
Node {id} <details>
<p>{JSON.stringify(data)}</p> <summary>JSON</summary>
<pre>{JSON.stringify(data, null, 2)}</pre>
</details>
<button type="button" onClick={() => setIsEditing((prev) => !prev)}>
{isEditing ? "done" : "edit"}
</button>
<div className={styles.mdContent} data-color-mode="light">
{
isEditing ? (
<>
<MDEditor
data-color-mode="light"
className={styles.mdEditor}
value={value}
onChange={setValue}
/>
</>
) : (
<>
<MDEditor.Markdown
source={value}
style={{ whiteSpace: "pre-wrap" }}
/>
</>
)
// <Markdown>{data.content}</Markdown>
}
</div>
</> </>
); );
} }

View file

@ -0,0 +1,9 @@
.menu {
background-color: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(10px);
// box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.25);
min-width: 500px;
min-height: 200px;
}

View file

@ -0,0 +1,66 @@
import styles from "./SearchBar.module.scss";
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
autoUpdate,
offset,
useClick,
useDismiss,
useFloating,
useFocus,
useInteractions,
} from "@floating-ui/react";
import { useState } from "react";
export default function SearchBar() {
const [showMenu, setShowMenu] = useState(() => false);
const { refs, context, floatingStyles } = useFloating({
placement: "bottom-start",
open: showMenu,
onOpenChange: setShowMenu,
whileElementsMounted: autoUpdate,
middleware: [offset(10)],
});
const focus = useFocus(context);
const { getReferenceProps, getFloatingProps } = useInteractions([
focus,
useDismiss(context),
]);
return (
<>
<div>
<input
type="text"
placeholder="Search..."
onFocus={() => setShowMenu(true)}
ref={refs.setReference}
{...getReferenceProps()}
/>
</div>
{showMenu && (
<FloatingPortal>
<FloatingOverlay>
{/* <FloatingFocusManager context={context} modal={false}> */}
<div
ref={refs.setFloating}
className={styles.menu}
style={{ ...floatingStyles }}
{...getFloatingProps()}
>
<SearchMenu />
</div>
{/* </FloatingFocusManager> */}
</FloatingOverlay>
</FloatingPortal>
)}
</>
);
}
function SearchMenu({}) {
return <>Search</>;
}

View file

@ -2,7 +2,7 @@ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use miette::{IntoDiagnostic, Report};
pub type AppResult<T, E = AppError> = std::result::Result<T, E>; pub type AppResult<T, E = AppError> = std::result::Result<T, E>;

View file

@ -1,10 +1,10 @@
use axum::{extract::State, Json}; use axum::{extract::State, Json};
use chrono::Local; use chrono::Local;
use cozo::{DataValue, ScriptMutability}; use cozo::ScriptMutability;
use serde_json::Value; use serde_json::Value;
use uuid::Uuid; use uuid::Uuid;
use crate::{ensure_ok, error::AppResult, AppState}; use crate::{error::AppResult, AppState};
pub async fn get_todays_journal_id( pub async fn get_todays_journal_id(
State(state): State<AppState>, State(state): State<AppState>,
@ -29,15 +29,15 @@ pub async fn get_todays_journal_id(
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
let node_id = uuid.to_string(); let node_id = uuid.to_string();
let result = state.db.run_script_fold_err( let _result = state.db.run_script_fold_err(
" "
{ {
?[id] <- [[$node_id]] ?[id, type] <- [[$node_id, 'panorama/journal/page']]
:put node { id } :put node { id, type }
} }
{ {
?[node_id, plaintext] <- [[$node_id, '']] ?[node_id, content] <- [[$node_id, 'Default **content**']]
:put journal { node_id => plaintext } :put journal { node_id => content }
} }
{ {
?[day, node_id] <- [[$day, $node_id]] ?[day, node_id] <- [[$day, $node_id]]
@ -45,11 +45,15 @@ pub async fn get_todays_journal_id(
} }
", ",
btmap! { btmap! {
"node_id".to_owned() => node_id.into(), "node_id".to_owned() => node_id.clone().into(),
"day".to_owned() => today.clone().into(), "day".to_owned() => today.clone().into(),
}, },
ScriptMutability::Mutable, ScriptMutability::Mutable,
); );
return Ok(Json(json!({
"node_id": node_id
})));
} }
let node_id = result.rows[0][0].get_str().unwrap(); let node_id = result.rows[0][0].get_str().unwrap();

View file

@ -1,6 +1,8 @@
#[macro_use] #[macro_use]
extern crate anyhow; extern crate anyhow;
#[macro_use] #[macro_use]
extern crate serde;
#[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use] #[macro_use]
extern crate sugars; extern crate sugars;
@ -13,7 +15,11 @@ mod node;
use std::fs; use std::fs;
use anyhow::Result; use anyhow::Result;
use axum::{http::Method, routing::get, Router}; use axum::{
http::Method,
routing::{get, post},
Router,
};
use cozo::DbInstance; use cozo::DbInstance;
use serde_json::Value; use serde_json::Value;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -21,7 +27,9 @@ use tower::ServiceBuilder;
use tower_http::cors::{self, CorsLayer}; use tower_http::cors::{self, CorsLayer};
use crate::{ use crate::{
journal::get_todays_journal_id, migrations::run_migrations, node::get_node, journal::get_todays_journal_id,
migrations::run_migrations,
node::{get_node, update_node},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -57,6 +65,7 @@ async fn main() -> Result<()> {
let app = Router::new() let app = Router::new()
.route("/", get(|| async { "Hello, World!" })) .route("/", get(|| async { "Hello, World!" }))
.route("/node/:id", get(get_node)) .route("/node/:id", get(get_node))
.route("/node/:id", post(update_node))
.route("/journal/get_todays_journal_id", get(get_todays_journal_id)) .route("/journal/get_todays_journal_id", get(get_todays_journal_id))
.layer(ServiceBuilder::new().layer(cors)) .layer(ServiceBuilder::new().layer(cors))
.with_state(state); .with_state(state);

View file

@ -1,6 +1,6 @@
use anyhow::{bail, Result}; use anyhow::Result;
use cozo::{DbInstance, ScriptMutability}; use cozo::DbInstance;
use futures::Future;
use serde_json::Value; use serde_json::Value;
use crate::ensure_ok; use crate::ensure_ok;
@ -9,11 +9,8 @@ pub async fn run_migrations(db: &DbInstance) -> Result<()> {
let migration_status = check_migration_status(db).await?; let migration_status = check_migration_status(db).await?;
println!("migration status: {:?}", migration_status); println!("migration status: {:?}", migration_status);
let migrations: Vec<Box<dyn for<'a> Fn(&'a DbInstance) -> Result<()>>> = vec![ let migrations: Vec<Box<dyn for<'a> Fn(&'a DbInstance) -> Result<()>>> =
Box::new(no_op), vec![Box::new(no_op), Box::new(migration_01)];
Box::new(migration_01),
Box::new(migration_02),
];
if let MigrationStatus::NoMigrations = migration_status { if let MigrationStatus::NoMigrations = migration_status {
let result = db.run_script_str( let result = db.run_script_str(
@ -108,34 +105,26 @@ fn migration_01(db: &DbInstance) -> Result<()> {
:create node { :create node {
id: String id: String
=> =>
type: String,
title: String? default null,
created_at: Float default now(), created_at: Float default now(),
updated_at: Float default now(), updated_at: Float default now(),
extra_data: Json default {}, extra_data: Json default {},
} }
} }
# Inverse mapping from keys to nodes # Inverse mappings for easy querying
{ :create has_key { key: String => id: String } } { :create node_has_key { key: String => id: String } }
{ :create node_managed_by_app { node_id: String => app: String } } { :create node_managed_by_app { node_id: String => app: String } }
", { :create node_refers_to { node_id: String => other_node_id: String } }
"",
false,
);
ensure_ok(&result)?;
Ok(())
}
fn migration_02(db: &DbInstance) -> Result<()> {
let result = db.run_script_str(
"
# Create journal type # Create journal type
{ :create journal { node_id: String => plaintext: String } } { :create journal { node_id: String => content: String } }
{ :create journal_days { day: String => node_id: String } } { :create journal_days { day: String => node_id: String } }
{ {
::fts create journal:text_index { ::fts create journal:text_index {
extractor: plaintext, extractor: content,
extract_filter: !is_null(plaintext), extract_filter: !is_null(content),
tokenizer: Simple, tokenizer: Simple,
filters: [Lowercase, Stemmer('english'), Stopwords('en')], filters: [Lowercase, Stemmer('english'), Stopwords('en')],
} }

View file

@ -1,5 +1,6 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
http::StatusCode,
Json, Json,
}; };
use cozo::ScriptMutability; use cozo::ScriptMutability;
@ -10,17 +11,19 @@ use crate::{error::AppResult, AppState};
pub async fn get_node( pub async fn get_node(
State(state): State<AppState>, State(state): State<AppState>,
Path(node_id): Path<String>, Path(node_id): Path<String>,
) -> AppResult<Json<Value>> { ) -> AppResult<(StatusCode, Json<Value>)> {
let result = state.db.run_script( let result = state.db.run_script(
" "
j[plaintext] := *journal{ node_id, plaintext }, node_id = $node_id j[content] := *journal{ node_id, content }, node_id = $node_id
j[plaintext] := not *journal{ node_id }, node_id = $node_id, plaintext = null j[content] := not *journal{ node_id }, node_id = $node_id, content = null
jd[day] := *journal_days{ node_id, day }, node_id = $node_id jd[day] := *journal_days{ node_id, day }, node_id = $node_id
jd[day] := not *journal_days{ node_id }, node_id = $node_id, day = null jd[day] := not *journal_days{ node_id }, node_id = $node_id, day = null
?[extra_data, plaintext, day] := *node{ id, extra_data }, ?[
j[plaintext], extra_data, content, day, created_at, updated_at, type
] := *node{ id, type, created_at, updated_at, extra_data },
j[content],
jd[day], jd[day],
id = $node_id id = $node_id
:limit 1 :limit 1
@ -29,15 +32,34 @@ pub async fn get_node(
ScriptMutability::Immutable, ScriptMutability::Immutable,
)?; )?;
if result.rows.len() == 0 {
return Ok((StatusCode::NOT_FOUND, Json(json!(null))));
}
let row = &result.rows[0]; let row = &result.rows[0];
let extra_data = row[0].get_str(); let extra_data = row[0].get_str();
let plaintext = row[1].get_str();
let day = row[2].get_str(); let day = row[2].get_str();
Ok(Json(json!({ Ok((
"node": node_id, StatusCode::OK,
"extra_data": extra_data, Json(json!({
"plaintext": plaintext, "node": node_id,
"day": day, "extra_data": extra_data,
}))) "content": row[1].get_str(),
"day": day,
"created_at": row[3].get_float(),
"updated_at": row[4].get_float(),
"type": row[5].get_str(),
})),
))
}
#[derive(Deserialize)]
struct NodeUpdate {}
pub async fn update_node(
State(state): State<AppState>,
Path(node_id): Path<String>,
) -> AppResult<Json<Value>> {
Ok(Json(json!({})))
} }

File diff suppressed because it is too large Load diff