Compare commits
No commits in common. "cf6e26b8902e53f017dc228c4febb924e92257c6" and "272bddab4f70d4b459c591e9c9da9cfcb22b6a66" have entirely different histories.
cf6e26b890
...
272bddab4f
@ -1,2 +0,0 @@
|
|||||||
/dist
|
|
||||||
/node_modules
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,4 +22,3 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
/.env
|
|
||||||
|
|||||||
30
Caddyfile
30
Caddyfile
@ -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
|
|
||||||
}
|
|
||||||
17
Dockerfile
17
Dockerfile
@ -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
|
|
||||||
11
package.json
11
package.json
@ -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
44
src/App.css
Normal 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;
|
||||||
|
}
|
||||||
232
src/App.tsx
232
src/App.tsx
@ -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>
|
||||||
|
</div>
|
||||||
{selected && <div className={"mt-2"}>
|
<h1>Vite + React</h1>
|
||||||
<div className="py-1">
|
<div className="card">
|
||||||
<Alias original={selected.label}/>
|
<button onClick={() => setCount((count) => count + 1)}>
|
||||||
</div>
|
count is {count}
|
||||||
|
</button>
|
||||||
{selected.data.aliases?.map(alias => (
|
<p>
|
||||||
<div className="py-1">
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
<Alias original={selected.label} alias={alias}/>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
<p className="read-the-docs">
|
||||||
|
Click on the Vite and React logos to learn more
|
||||||
<div className="py-1">
|
</p>
|
||||||
<CustomAlias selected={selected}/>
|
</>
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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>,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,8 +7,6 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [],
|
||||||
require('@tailwindcss/forms')
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
plugins: [react()],
|
||||||
// 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']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user