diff --git a/.vscode/settings.json b/.vscode/settings.json index 1244a7e..c613a25 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,10 @@ { "biome.enabled": true, "editor.defaultFormatter": "biomejs.biome", - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit" + }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" }, diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index d8cd091..b0c6e36 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -21,6 +21,8 @@ services: env_file: .env.docker ports: - 3040:3040 + volumes: + - ./.env.docker:/app/.env depends_on: database: condition: service_healthy diff --git a/frontend/biome.json b/frontend/biome.json index f8702e4..1c90366 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -8,5 +8,10 @@ "rules": { "recommended": true } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 4 } } diff --git a/frontend/package.json b/frontend/package.json index 6214062..42de99c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,41 +1,41 @@ { - "name": "haystack", - "version": "0.1.0", - "description": "", - "type": "module", - "scripts": { - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "tauri": "tauri", - "lint": "bunx @biomejs/biome lint .", - "format": "bunx @biomejs/biome format . --write" - }, - "license": "MIT", - "dependencies": { - "@kobalte/core": "^0.13.9", - "@kobalte/tailwindcss": "^0.9.0", - "@solidjs/router": "^0.15.3", - "@tabler/icons-solidjs": "^3.30.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "~2", - "@tauri-apps/plugin-opener": "^2", - "clsx": "^2.1.1", - "fuse.js": "^7.1.0", - "solid-js": "^1.9.3", - "tailwind-scrollbar-hide": "^2.0.0", - "valibot": "^1.0.0-rc.2" - }, - "devDependencies": { - "@biomejs/biome": "^1.9.4", - "@tauri-apps/cli": "^2", - "autoprefixer": "^10.4.20", - "postcss": "^8.5.3", - "postcss-cli": "^11.0.0", - "tailwindcss": "3.4.0", - "typescript": "~5.6.2", - "vite": "^6.0.3", - "vite-plugin-solid": "^2.11.0" - } + "name": "haystack", + "version": "0.1.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "tauri": "tauri", + "lint": "bunx @biomejs/biome lint .", + "format": "bunx @biomejs/biome format . --write" + }, + "license": "MIT", + "dependencies": { + "@kobalte/core": "^0.13.9", + "@kobalte/tailwindcss": "^0.9.0", + "@solidjs/router": "^0.15.3", + "@tabler/icons-solidjs": "^3.30.0", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-opener": "^2", + "clsx": "^2.1.1", + "fuse.js": "^7.1.0", + "solid-js": "^1.9.3", + "tailwind-scrollbar-hide": "^2.0.0", + "valibot": "^1.0.0-rc.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@tauri-apps/cli": "^2", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.3", + "postcss-cli": "^11.0.0", + "tailwindcss": "3.4.0", + "typescript": "~5.6.2", + "vite": "^6.0.3", + "vite-plugin-solid": "^2.11.0" + } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 7b75c83..49c0612 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index d8e24c2..d21ade3 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -1,12 +1,12 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], - "permissions": [ - "core:default", - "opener:default", - "dialog:default", - "core:window:allow-start-dragging" - ] + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "dialog:default", + "core:window:allow-start-dragging" + ] } diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index d056e73..fa1b1bc 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,30 +1,30 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "haystack", - "version": "0.1.0", - "identifier": "com.haystack.app", - "build": { - "beforeDevCommand": "bun run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "bun run build", - "frontendDist": "../dist" - }, - "app": { - "windows": [], - "macOSPrivateApi": true, - "security": { - "csp": null - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } + "$schema": "https://schema.tauri.app/config/2", + "productName": "haystack", + "version": "0.1.0", + "identifier": "com.haystack.app", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [], + "macOSPrivateApi": true, + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 07eecb8..a3a716b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,132 +1,148 @@ -import { createEffect, createResource, createSignal, For } from "solid-js"; import { Search } from "@kobalte/core/search"; -import { IconSearch, IconRefresh } from "@tabler/icons-solidjs"; +import { A, useNavigate } from "@solidjs/router"; +import { IconRefresh, IconSearch } from "@tabler/icons-solidjs"; +import { image } from "@tauri-apps/api"; import clsx from "clsx"; +import Fuse from "fuse.js"; +import { For, createEffect, createResource, createSignal } from "solid-js"; import { ImageViewer } from "./components/ImageViewer"; import { getUserImages } from "./network"; -import { image } from "@tauri-apps/api"; -import { A, useNavigate } from "@solidjs/router"; -import Fuse from "fuse.js"; type UserImages = Awaited>; function App() { - const [searchResults, setSearchResults] = createSignal([]); + const [searchResults, setSearchResults] = createSignal< + UserImages[number]["Text"] + >([]); - const [images] = createResource(getUserImages); + const [images] = createResource(getUserImages); - const nav = useNavigate(); + const nav = useNavigate(); - let fuze = new Fuse[number]>([], { keys: ["Text.ImageText"] }); + let fuze = new Fuse[number]>([], { + keys: ["Text.ImageText"], + }); - // TODO: there's probably a better way? - createEffect(() => { - const userImages = images(); - if (userImages == null) { - return; - } + // TODO: there's probably a better way? + createEffect(() => { + const userImages = images(); - const imageText = userImages.flatMap(i => i.Text ?? []); + console.log(userImages); + if (userImages == null) { + return; + } - fuze = new Fuse(imageText, { keys: ["ImageText"], threshold: 0.3 }); - }); + const imageText = userImages.flatMap((i) => i.Text ?? []); - const onInputChange = (query: string) => { - // TODO: we can migrate this searching to Rust, so we don't abuse the main thread. - // But, it's not too bad as is. - setSearchResults(fuze.search(query).flatMap(s => s.item)); - } + fuze = new Fuse(imageText, { + keys: ["ImageText"], + threshold: 0.3, + }); + }); - return ( -
-
- { - if (item?.ImageID == null) { - console.error("ImageID was null"); - return; - } + const onInputChange = (query: string) => { + // TODO: we can migrate this searching to Rust, so we don't abuse the main thread. + // But, it's not too bad as is. + setSearchResults(fuze.search(query).flatMap((s) => s.item)); + }; - nav(`/image/${item.ImageID}`); - }} - optionValue="ID" - optionLabel="ImageText" - placeholder="Search for stuff..." - itemComponent={(props) => ( - - - {props.item.rawValue.ImageText ?? ''} - - - )} - > - - - - - } - > - - - - - - - - e.preventDefault()} - > - - - 😬 No emoji found - - - - -
- {/*
+ return ( +
+
+ { + if (item?.ImageID == null) { + console.error("ImageID was null"); + return; + } + + nav(`/image/${item.ImageID}`); + }} + optionValue="ID" + optionLabel="ImageText" + placeholder="Search for stuff..." + itemComponent={(props) => ( + + + {props.item.rawValue.ImageText ?? ""} + + + )} + > + + + + + } + > + + + + + + + + e.preventDefault()} + > + + + 😬 No emoji found + + + + +
+ {/*
Emoji selected: {emoji()?.emoji} {emoji()?.name}
*/} - -
-
-
-
-
-
-
-
-
-
- - {(image) => ( - - )} - -
-
-
-
- footer -
-
- ); + +
+
+
+ {/*
+
+
+
+
+
+
*/} + {/* {JSON.stringify(images())} */} + + {(image) => ( + + + + )} + +
+
+
+
+ footer +
+
+ ); } export default App; diff --git a/frontend/src/ImagePage.tsx b/frontend/src/ImagePage.tsx index 2173ef7..0df3f4b 100644 --- a/frontend/src/ImagePage.tsx +++ b/frontend/src/ImagePage.tsx @@ -1,14 +1,14 @@ -import { A, useParams } from "@solidjs/router" -import { createEffect, createResource, For, Suspense } from "solid-js" -import { getUserImages } from "./network" +import { A, useParams } from "@solidjs/router"; +import { createEffect, createResource, For, Suspense } from "solid-js"; +import { getUserImages } from "./network"; export function ImagePage() { - const { imageId } = useParams<{ imageId: string }>() + const { imageId } = useParams<{ imageId: string }>(); const [image] = createResource(async () => { const userImages = await getUserImages(); - const currentImage = userImages.find(image => image.ID === imageId); + const currentImage = userImages.find((image) => image.ID === imageId); if (currentImage == null) { // TODO: this error handling. throw new Error("must be valid"); @@ -19,18 +19,18 @@ export function ImagePage() { createEffect(() => { console.log(image()); - }) + }); - return (Loading...}> - Back -

{image()?.Image.ImageName}

- -
- - {(tag) => ( -
{tag.Tag}
- )} -
-
-
) + return ( + Loading...}> + Back +

{image()?.Image.ImageName}

+ +
+ + {(tag) =>
{tag.Tag}
} +
+
+
+ ); } diff --git a/frontend/src/components/FolderPicker.tsx b/frontend/src/components/FolderPicker.tsx index 285a2bb..80ea73b 100644 --- a/frontend/src/components/FolderPicker.tsx +++ b/frontend/src/components/FolderPicker.tsx @@ -3,47 +3,47 @@ import { open } from "@tauri-apps/plugin-dialog"; import { invoke } from "@tauri-apps/api/core"; export function FolderPicker() { - const [selectedPath, setSelectedPath] = createSignal(""); - const [status, setStatus] = createSignal(""); + const [selectedPath, setSelectedPath] = createSignal(""); + const [status, setStatus] = createSignal(""); - const handleFolderSelect = async () => { - try { - const selected = await open({ - directory: true, - multiple: false, - }); + const handleFolderSelect = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + }); - if (selected) { - setSelectedPath(selected as string); - // Send the path to Rust - const response = await invoke("handle_selected_folder", { - path: selected, - }); - setStatus(`Folder processed: ${response}`); - } - } catch (error) { - setStatus(`Error: ${error}`); - } - }; + if (selected) { + setSelectedPath(selected as string); + // Send the path to Rust + const response = await invoke("handle_selected_folder", { + path: selected, + }); + setStatus(`Folder processed: ${response}`); + } + } catch (error) { + setStatus(`Error: ${error}`); + } + }; - return ( -
- + return ( +
+ - {selectedPath() && ( -
-

Selected folder:

-

{selectedPath()}

-
- )} + {selectedPath() && ( +
+

Selected folder:

+

{selectedPath()}

+
+ )} - {status() &&

{status()}

} -
- ); + {status() &&

{status()}

} +
+ ); } diff --git a/frontend/src/index.css b/frontend/src/index.css index d61995c..3deaf14 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,21 +3,21 @@ @tailwind utilities; @font-face { - font-family: "Manrope"; - src: url("./assets/fonts/Manrope-VariableFont_wght.ttf") format("truetype"); - font-weight: 100 900; - font-display: swap; + font-family: "Manrope"; + src: url("./assets/fonts/Manrope-VariableFont_wght.ttf") format("truetype"); + font-weight: 100 900; + font-display: swap; } :root { - @apply bg-neutral-100 text-black rounded-xl; - font-family: Manrope, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 500; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; + @apply bg-neutral-100 text-black rounded-xl; + font-family: Manrope, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 500; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 13e8ce0..c5cd292 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -5,9 +5,12 @@ import "./index.css"; import { Route, Router } from "@solidjs/router"; import { ImagePage } from "./ImagePage"; -render(() => ( - - - - -), document.getElementById("root") as HTMLElement); +render( + () => ( + + + + + ), + document.getElementById("root") as HTMLElement, +); diff --git a/frontend/src/network/index.ts b/frontend/src/network/index.ts index 8b22a4b..a51753e 100644 --- a/frontend/src/network/index.ts +++ b/frontend/src/network/index.ts @@ -1,96 +1,81 @@ import { - array, - InferOutput, - null as Null, - nullable, - object, - parse, - pipe, - string, - uuid, + type InferOutput, + null as Null, + any, + array, + nullable, + object, + parse, + pipe, + string, + uuid, } from "valibot"; type BaseRequestParams = Partial<{ - path: string; - body: RequestInit["body"]; - method: "GET" | "POST"; + path: string; + body: RequestInit["body"]; + method: "GET" | "POST"; }>; const getBaseRequest = ({ path, body, method }: BaseRequestParams): Request => { - return new Request(`http://localhost:3040/${path}`, { - headers: { userId: "fcc22dbb-7792-4595-be8e-d0439e13990a" }, - body, - method, - }); + return new Request(`http://localhost:3040/${path}`, { + headers: { userId: "fcc22dbb-7792-4595-be8e-d0439e13990a" }, + body, + method, + }); }; const sendImageResponseValidator = object({ - ID: pipe(string(), uuid()), - ImageID: pipe(string(), uuid()), - UserID: pipe(string(), uuid()), + ID: pipe(string(), uuid()), + ImageID: pipe(string(), uuid()), + UserID: pipe(string(), uuid()), }); export const sendImage = async ( - imageName: string, - base64Image: string, + imageName: string, + base64Image: string, ): Promise> => { - const request = getBaseRequest({ - path: `image/${imageName}`, - body: base64Image, - method: "POST", - }); + const request = getBaseRequest({ + path: `image/${imageName}`, + body: base64Image, + method: "POST", + }); - request.headers.set("Content-Type", "application/base64"); + request.headers.set("Content-Type", "application/base64"); - const res = await fetch(request).then((res) => res.json()); + const res = await fetch(request).then((res) => res.json()); - return parse(sendImageResponseValidator, res); + return parse(sendImageResponseValidator, res); }; const getUserImagesResponseValidator = array( - object({ - ID: pipe(string(), uuid()), - Image: object({ - ID: pipe(string(), uuid()), - ImageName: string(), - Image: Null(), - }), - Tags: nullable( - array( - object({ - ID: pipe(string(), uuid()), - Tag: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - Links: nullable( - array( - object({ - ID: pipe(string(), uuid()), - Links: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - Text: nullable( - array( - object({ - ID: pipe(string(), uuid()), - ImageText: string(), - ImageID: pipe(string(), uuid()), - }), - ), - ), - }), + object({ + ID: pipe(string(), uuid()), + Image: object({ + ID: pipe(string(), uuid()), + ImageName: string(), + Image: Null(), + }), + Tags: any(), + Links: any(), + Text: nullable( + array( + object({ + ID: pipe(string(), uuid()), + ImageText: string(), + ImageID: pipe(string(), uuid()), + }), + ), + ), + }), ); export const getUserImages = async (): Promise< - InferOutput + InferOutput > => { - const request = getBaseRequest({ path: "image" }); + const request = getBaseRequest({ path: "image" }); - const res = await fetch(request).then((res) => res.json()); + const res = await fetch(request).then((res) => res.json()); - return parse(getUserImagesResponseValidator, res); + return parse(getUserImagesResponseValidator, res); }; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index f749829..2aede78 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,15 +1,15 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], - theme: { - extend: { - fontFamily: { - sans: ["Manrope", "sans-serif"], - }, - }, - }, - plugins: [ - require("@kobalte/tailwindcss"), - require("tailwind-scrollbar-hide"), - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + fontFamily: { + sans: ["Manrope", "sans-serif"], + }, + }, + }, + plugins: [ + require("@kobalte/tailwindcss"), + require("tailwind-scrollbar-hide"), + ], }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index f7f13c7..880e8e2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,26 +1,26 @@ { - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "skipLibCheck": true, + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index eca6668..26063d8 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 815ec0f..5cd5350 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -8,31 +8,31 @@ const host = process.env.TAURI_DEV_HOST; // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [solid()], - css: { - postcss: { - plugins: [tailwindcss, autoprefixer], - }, - }, - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - // 3. tell vite to ignore watching `src-tauri` - ignored: ["**/src-tauri/**"], - }, - }, + plugins: [solid()], + css: { + postcss: { + plugins: [tailwindcss, autoprefixer], + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, }));