Compare commits

..

No commits in common. "cf6e26b8902e53f017dc228c4febb924e92257c6" and "272bddab4f70d4b459c591e9c9da9cfcb22b6a66" have entirely different histories.

11 changed files with 80 additions and 290 deletions

View File

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

1
.gitignore vendored
View File

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

View File

@ -1,30 +0,0 @@
# 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
}

View File

@ -1,17 +0,0 @@
# 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,21 +10,12 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "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": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0"
"react-select": "^5.7.5",
"react-window": "^1.8.9"
}, },
"devDependencies": { "devDependencies": {
"@types/ramda": "^0.29.5",
"@types/react": "^18.0.37", "@types/react": "^18.0.37",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-window": "^1.8.6",
"@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0", "@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react-swc": "^3.0.0", "@vitejs/plugin-react-swc": "^3.0.0",

44
src/App.css Normal file
View File

@ -0,0 +1,44 @@
#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,209 +1,35 @@
import { import {useState} from 'react'
ChangeEventHandler, Dispatch, import reactLogo from './assets/react.svg'
KeyboardEventHandler, import viteLogo from '/vite.svg'
MouseEventHandler, import './App.css'
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';
import {useQuery} from "@tanstack/react-query"; function App() {
const [count, setCount] = useState(0)
// 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 ( return (
<div className={"m-5 text-lg"}> <>
<Select <div>
onChange={onChange as any} <a href="https://vitejs.dev" target="_blank">
components={{MenuList}} <img src={viteLogo} className="logo" alt="Vite logo"/>
isLoading={isLoading} </a>
options={options} <a href="https://react.dev" target="_blank">
filterOption={createFilter({ignoreAccents: false})} <img src={reactLogo} className="logo react" alt="React logo"/>
/> </a>
{selected && <div className={"mt-2"}>
<div className="py-1">
<Alias original={selected.label}/>
</div> </div>
<h1>Vite + React</h1>
{selected.data.aliases?.map(alias => ( <div className="card">
<div className="py-1"> <button onClick={() => setCount((count) => count + 1)}>
<Alias original={selected.label} alias={alias}/> count is {count}
</div> </button>
))} <p>
Edit <code>src/App.tsx</code> and save to test HMR
<div className="py-1"> </p>
<CustomAlias selected={selected}/>
</div>
</div>}
</div> </div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
) )
} }
function CustomAlias({selected}: { export default App
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,15 +1,10 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css' 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( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <App/>
<LargeSelect/>
</QueryClientProvider>
</React.StrictMode>, </React.StrictMode>,
) )

View File

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

View File

@ -1,21 +1,7 @@
import { defineConfig, loadEnv } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => { export default defineConfig({
// 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()], plugins: [react()],
server: {
host: '0.0.0.0',
proxy: {
'/metadata': {
target: env['OBSIDIAN_BEACHHEAD_SERVER']
}
}
}
}
}) })