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"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@floating-ui/react": "^0.26.16",
"@fontsource/inter": "^5.0.18",
"@mui/icons-material": "^5.15.18",
"@mui/material": "^5.15.18",
"@tanstack/react-query": "^5.37.1",
"@tauri-apps/api": "^1",
"@uiw/react-md-editor": "^4.0.4",
"javascript-time-ago": "^2.5.10",
"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": {
"@tauri-apps/cli": "^1",

View file

@ -6,9 +6,13 @@ import "./global.scss";
import { useEffect, useState } from "react";
import NodeDisplay from "./components/NodeDisplay";
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();
TimeAgo.addDefaultLocale(en);
function App() {
const [nodesOpened, setNodesOpened] = useState<string[]>(() => []);

View file

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

View file

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

View file

@ -1,5 +1,9 @@
import { useQuery } from "@tanstack/react-query";
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 {
id: string;
@ -19,7 +23,11 @@ export default function NodeDisplay({ id }: NodeDisplayProps) {
return (
<div className={styles.container}>
<div className={styles.header}>
<small>ID {id}</small>
{isSuccess ? (
<NodeDisplayHeaderLoaded id={id} data={data} />
) : (
<small>ID {id}</small>
)}
</div>
<div className={styles.body}>
{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 }) {
const [value, setValue] = useState(() => data.content);
const [isEditing, setIsEditing] = useState(() => false);
return (
<>
Node {id}
<p>{JSON.stringify(data)}</p>
<details>
<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,
response::{IntoResponse, Response},
};
use miette::{IntoDiagnostic, Report};
pub type AppResult<T, E = AppError> = std::result::Result<T, E>;

View file

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

View file

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

View file

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

View file

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