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 | ||||
| *.sln | ||||
| *.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" | ||||
|   }, | ||||
|   "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-select": "^5.7.5", | ||||
|     "react-window": "^1.8.9" | ||||
|     "react-dom": "^18.2.0" | ||||
|   }, | ||||
|   "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", | ||||
|  | ||||
							
								
								
									
										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 { | ||||
|     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'; | ||||
| import {useState} from 'react' | ||||
| import reactLogo from './assets/react.svg' | ||||
| import viteLogo from '/vite.svg' | ||||
| import './App.css' | ||||
| 
 | ||||
| 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}]]`) | ||||
|     }, []); | ||||
| function App() { | ||||
|     const [count, setCount] = useState(0) | ||||
| 
 | ||||
|     return ( | ||||
|         <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> | ||||
|         <> | ||||
|             <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> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| 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> | ||||
|     ); | ||||
| } | ||||
| export default App | ||||
|  | ||||
| @ -1,15 +1,10 @@ | ||||
| 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> | ||||
|         <QueryClientProvider client={queryClient}> | ||||
|             <LargeSelect/> | ||||
|         </QueryClientProvider> | ||||
|         <App/> | ||||
|     </React.StrictMode>, | ||||
| ) | ||||
|  | ||||
| @ -7,8 +7,6 @@ export default { | ||||
|   theme: { | ||||
|     extend: {}, | ||||
|   }, | ||||
|   plugins: [ | ||||
|     require('@tailwindcss/forms') | ||||
|   ], | ||||
|   plugins: [], | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,21 +1,7 @@ | ||||
| import { defineConfig, loadEnv } from 'vite' | ||||
| import { defineConfig } from 'vite' | ||||
| import react from '@vitejs/plugin-react-swc' | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| 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'] | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| export default defineConfig({ | ||||
|   plugins: [react()], | ||||
| }) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user