Compare commits

...

10 Commits

11 changed files with 290 additions and 80 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
/dist
/node_modules

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/.env

30
Caddyfile Normal file
View File

@ -0,0 +1,30 @@
# global options
{
admin off # theres no need for the admin api in railway's environment
persist_config off # storage isn't persistent anyway
auto_https off # railway handles https for us, this would cause issues if left enabled
log { # runtime logs
format console # set runtime log format to console mode
}
}
:3000 {
log { # access logs
format console # set access log format to console mode
}
# health check for railway
respond /health 200
# serve from the 'dist' folder (Vite builds into the 'dist' folder)
root * /app/dist
# enable gzipping responses
encode gzip
# serve files from 'dist'
file_server
# if path doesn't exist, redirect it to 'index.html' for client side routing
try_files {path} /index.html
}

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
FROM oven/bun:1 AS deps
WORKDIR /app
COPY package.json package.json
COPY bun.lockb bun.lockb
RUN bun install
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM caddy AS runner
COPY --from=builder /app/dist /usr/share/caddy

BIN
bun.lockb

Binary file not shown.

View File

@ -10,12 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@tailwindcss/forms": "^0.5.6",
"@tanstack/react-query": "^4.35.3",
"ramda": "^0.29.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-select": "^5.7.5",
"react-window": "^1.8.9"
},
"devDependencies": {
"@types/ramda": "^0.29.5",
"@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11",
"@types/react-window": "^1.8.6",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.0.0",

View File

@ -1,44 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@ -1,35 +1,209 @@
import {useState} from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import {
ChangeEventHandler, Dispatch,
KeyboardEventHandler,
MouseEventHandler,
ReactNode, SetStateAction,
useCallback,
useEffect,
useRef,
useState
} from "react";
import {FixedSizeList as List} from "react-window";
import Select, {createFilter, MenuListProps} from "react-select";
import * as R from 'ramda';
function App() {
const [count, setCount] = useState(0)
import {useQuery} from "@tanstack/react-query";
// TODO: Fix this for wrapping items, esp on phones
const height = 35;
function MenuList(props: MenuListProps) {
const {
options,
children,
maxHeight,
getValue
} = props as Omit<MenuListProps, 'children'> & {
children: ReactNode[]
};
const [value] = getValue();
const initialOffset = options.indexOf(value) * height;
return (<List
width={'100%'}
height={maxHeight}
itemCount={children.length}
itemSize={height}
initialScrollOffset={initialOffset}
>
{({
index,
style
}) => <div style={style}>{children[index]}</div>}
</List>);
}
interface Option {
label: string,
value: string,
data: {
backlinks: unknown[]
aliases?: string[]
}
}
export function LargeSelect() {
const {
data: options,
isLoading,
} = useQuery({
queryKey: ['obsidian-metadata'],
initialData: [],
queryFn: async () => {
const response = await fetch("/metadata")
const fullData: any[] = await response.json();
return R.sortBy(v => -(v.data.backlinks?.length ?? 0), fullData.map(md => ({
value: md.relativePath,
label: md.fileName,
data: md,
}) as Option));
},
});
const [selected, setSelected] = useState<Option | null>(null);
const onChange = useCallback((value: Option) => {
setSelected(value);
navigator.clipboard.writeText(`[[${value.label}]]`)
}, []);
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo"/>
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo"/>
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
<div className={"m-5 text-lg"}>
<Select
onChange={onChange as any}
components={{MenuList}}
isLoading={isLoading}
options={options}
filterOption={createFilter({ignoreAccents: false})}
/>
{selected && <div className={"mt-2"}>
<div className="py-1">
<Alias original={selected.label}/>
</div>
{selected.data.aliases?.map(alias => (
<div className="py-1">
<Alias original={selected.label} alias={alias}/>
</div>
))}
<div className="py-1">
<CustomAlias selected={selected}/>
</div>
</div>}
</div>
)
}
export default App
function CustomAlias({selected}: {
selected: Option
}) {
const [alias, setAlias] = useState('');
// Reset when selection changes
useEffect(() => {
setAlias('');
}, [selected.value]);
const onClick: MouseEventHandler = useCallback((e) => {
if ((e.target as HTMLElement).tagName == 'SPAN' && alias.length > 0) {
navigator.clipboard.writeText(`[[${selected!.label}|${alias}]]`);
}
}, [selected.value, selected.label, alias]);
return (
<span onClick={onClick} className={"rounded-md p-1 hover:bg-slate-100"}>
<span className={"text-slate-300 p-0.5"}>[[</span>
<span>{selected.label}</span>
<span className={"text-slate-300 p-0.5"}>|</span>
<span>
<CustomAliasField content={alias} setContent={setAlias} selected={selected}/>
</span>
<span className={"text-slate-300 p-0.5"}>]]</span>
</span>
)
}
function CustomAliasField({
selected,
content,
setContent
}: {
selected: Option,
content: string,
setContent: Dispatch<SetStateAction<string>>,
}) {
const [width, setWidth] = useState<any>(0);
const span = useRef<HTMLSpanElement>(null);
// Resize on change of content
useEffect(() => {
setWidth(span.current!.offsetWidth);
// setWidth(content.length + 'ch');
}, [content]);
const changeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(evt => {
setContent(evt.target.value);
}, []);
const onCustomElementKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
if (e.key == 'Enter') {
navigator.clipboard.writeText(`[[${selected!.label}|${content}]]`);
(e.target as HTMLInputElement).blur()
}
}, [selected.value, selected.label, content]);
return (
<span>
<span style={{
position: 'absolute',
opacity: 0,
zIndex: -100,
whiteSpace: 'pre',
}} ref={span}>{content}</span>
<input
className={"border-none p-0 px-1"}
type="text" style={{width: `calc(${width}px + 0.25rem)`}} autoFocus onChange={changeHandler}
onKeyDown={onCustomElementKeyDown}/>
</span>
);
}
function Alias({
original,
alias
}: {
original: string,
alias?: string
}) {
const onClick = useCallback(() => {
if (alias) {
navigator.clipboard.writeText(`[[${original}|${alias}]]`)
} else {
navigator.clipboard.writeText(`[[${original}]]`)
}
}, [original, alias]);
return (
<span className={"rounded-md p-1 hover:bg-slate-100"} onClick={onClick}>
<span className={"text-slate-300 p-0.5"}>[[</span>
<span>{original}</span>
{alias && <><span className={"text-slate-300 p-0.5"}>|</span>
<span>{alias}</span></>}
<span className={"text-slate-300 p-0.5"}>]]</span>
</span>
);
}

View File

@ -1,10 +1,15 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {LargeSelect} from "./App.tsx";
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App/>
<QueryClientProvider client={queryClient}>
<LargeSelect/>
</QueryClientProvider>
</React.StrictMode>,
)

View File

@ -7,6 +7,8 @@ export default {
theme: {
extend: {},
},
plugins: [],
plugins: [
require('@tailwindcss/forms')
],
}

View File

@ -1,7 +1,21 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
export default defineConfig(({ command, mode }) => {
// Load env file based on `mode` in the current working directory.
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [react()],
server: {
host: '0.0.0.0',
proxy: {
'/metadata': {
target: env['OBSIDIAN_BEACHHEAD_SERVER']
}
}
}
}
})