feat: allowing user to get a list of their images
feat: UI to show and organise user images
This commit is contained in:
@@ -3,7 +3,13 @@ import type { PluginListener } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||
import {
|
||||
type Component,
|
||||
type ParentProps,
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
} from "solid-js";
|
||||
import {
|
||||
type ShareEvent,
|
||||
listenForShareEvents,
|
||||
@@ -20,6 +26,10 @@ import { type sendImage, sendImageFile } from "./network";
|
||||
const currentPlatform = platform();
|
||||
console.log("Current Platform: ", currentPlatform);
|
||||
|
||||
const AppWrapper: Component<ParentProps> = ({ children }) => {
|
||||
return <div class="flex w-full justify-center h-screen">{children}</div>;
|
||||
};
|
||||
|
||||
export const App = () => {
|
||||
createEffect(() => {
|
||||
// TODO: Don't use window.location.href
|
||||
@@ -79,11 +89,13 @@ export const App = () => {
|
||||
onSetProcessingImage={setProcessingImage}
|
||||
/>
|
||||
<Router>
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/" component={AppWrapper}>
|
||||
<Route path="/login" component={Login} />
|
||||
|
||||
<Route path="/" component={ProtectedRoute}>
|
||||
<Route path="/" component={Search} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/" component={ProtectedRoute}>
|
||||
<Route path="/" component={Search} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
</SearchImageContextProvider>
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SearchCard } from "./components/search-card/SearchCard";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { ItemModal } from "./components/item-modal/ItemModal";
|
||||
import type { Shortcut } from "./components/shortcuts/hooks/useShortcutEditor";
|
||||
import type { UserImage } from "./network";
|
||||
import { base, type UserImage } from "./network";
|
||||
import { useSearchImageContext } from "./contexts/SearchImageContext";
|
||||
|
||||
export const Search = () => {
|
||||
@@ -24,9 +24,10 @@ export const Search = () => {
|
||||
null,
|
||||
);
|
||||
|
||||
const { images, onRefetchImages } = useSearchImageContext();
|
||||
const { images, imagesWithProperties, onRefetchImages } =
|
||||
useSearchImageContext();
|
||||
|
||||
let fuze = new Fuse<UserImage>(images() ?? [], {
|
||||
let fuze = new Fuse<UserImage>(images(), {
|
||||
keys: [
|
||||
{ name: "rawData", weight: 1 },
|
||||
{ name: "title", weight: 1 },
|
||||
@@ -35,11 +36,9 @@ export const Search = () => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
console.log("DBG: ", images());
|
||||
setSearchResults(images() ?? []);
|
||||
console.log(images());
|
||||
setSearchResults(images());
|
||||
|
||||
fuze = new Fuse<UserImage>(images() ?? [], {
|
||||
fuze = new Fuse<UserImage>(images(), {
|
||||
keys: [
|
||||
{ name: "data.Name", weight: 2 },
|
||||
{ name: "rawData", weight: 1 },
|
||||
@@ -50,10 +49,9 @@ export const Search = () => {
|
||||
|
||||
const onInputChange = (event: InputEvent) => {
|
||||
const query = (event.target as HTMLInputElement).value;
|
||||
console.log(query);
|
||||
|
||||
if (query.length === 0) {
|
||||
setSearchResults(images() ?? []);
|
||||
setSearchResults(images());
|
||||
} else {
|
||||
setSearchQuery(query);
|
||||
setSearchResults(fuze.search(query).map((s) => s.item));
|
||||
@@ -134,7 +132,7 @@ export const Search = () => {
|
||||
</div>
|
||||
|
||||
<div class="px-4 mt-4 bg-white rounded-t-2xl">
|
||||
<div class="h-[254px] mt-4 overflow-scroll scrollbar-hide">
|
||||
<div class="mt-4 overflow-scroll scrollbar-hide">
|
||||
<Show
|
||||
when={searchResults().length > 0}
|
||||
fallback={
|
||||
@@ -169,6 +167,19 @@ export const Search = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full grid grid-cols-9 gap-2 grid-flow-row-dense py-4">
|
||||
<For each={Object.keys(imagesWithProperties())}>
|
||||
{(imageId) => (
|
||||
<div>
|
||||
<img
|
||||
alt="One of the users images"
|
||||
src={`${base}/image/${imageId}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="w-full border-t h-10 bg-white px-4 flex items-center border-neutral-100">
|
||||
<p class="text-sm text-neutral-700">
|
||||
Use{" "}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import {
|
||||
createContext,
|
||||
type Resource,
|
||||
type Accessor,
|
||||
type Component,
|
||||
type ParentProps,
|
||||
createContext,
|
||||
createMemo,
|
||||
createResource,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { getUserImages } from "../network";
|
||||
import { groupPropertiesWithImage } from "../utils/groupPropertiesWithImage";
|
||||
|
||||
type ImageWithRawData = Awaited<ReturnType<typeof getUserImages>>[number] & {
|
||||
export type ImageWithRawData = Awaited<
|
||||
ReturnType<typeof getUserImages>
|
||||
>["ImageProperties"][number] & {
|
||||
rawData: string[];
|
||||
};
|
||||
|
||||
type SearchImageStore = {
|
||||
images: Resource<ImageWithRawData[]>;
|
||||
images: Accessor<ImageWithRawData[]>;
|
||||
|
||||
imagesWithProperties: Accessor<ReturnType<typeof groupPropertiesWithImage>>;
|
||||
onRefetchImages: () => void;
|
||||
};
|
||||
|
||||
@@ -50,19 +56,36 @@ const getAllValues = (object: object): Array<string> => {
|
||||
|
||||
const SearchImageContext = createContext<SearchImageStore>();
|
||||
export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
||||
const [images, { refetch }] = createResource(() =>
|
||||
getUserImages().then((data) => {
|
||||
return data.map((d) => ({
|
||||
...d,
|
||||
rawData: getAllValues(d),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
const [data, { refetch }] = createResource(getUserImages);
|
||||
|
||||
const imageData = createMemo<ImageWithRawData[]>(() => {
|
||||
const d = data();
|
||||
if (d == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return d.ImageProperties.map((d) => ({
|
||||
...d,
|
||||
rawData: getAllValues(d),
|
||||
}));
|
||||
});
|
||||
|
||||
const imagesWithProperties = createMemo<
|
||||
ReturnType<typeof groupPropertiesWithImage>
|
||||
>(() => {
|
||||
const d = data();
|
||||
if (d == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return groupPropertiesWithImage(d);
|
||||
});
|
||||
|
||||
return (
|
||||
<SearchImageContext.Provider
|
||||
value={{
|
||||
images,
|
||||
images: imageData,
|
||||
imagesWithProperties: imagesWithProperties,
|
||||
onRefetchImages: refetch,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type InferOutput,
|
||||
array,
|
||||
literal,
|
||||
null_,
|
||||
nullable,
|
||||
parse,
|
||||
pipe,
|
||||
@@ -84,6 +85,7 @@ export const sendImage = async (
|
||||
|
||||
const locationValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
Address: nullable(string()),
|
||||
Description: nullable(string()),
|
||||
@@ -92,6 +94,7 @@ const locationValidator = strictObject({
|
||||
|
||||
const contactValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
Description: nullable(string()),
|
||||
PhoneNumber: nullable(string()),
|
||||
@@ -101,6 +104,7 @@ const contactValidator = strictObject({
|
||||
|
||||
const eventValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
StartDateTime: nullable(pipe(string())),
|
||||
EndDateTime: nullable(pipe(string())),
|
||||
@@ -114,6 +118,7 @@ const eventValidator = strictObject({
|
||||
|
||||
const noteValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
Description: nullable(string()),
|
||||
Content: string(),
|
||||
@@ -146,18 +151,36 @@ const dataTypeValidator = variant("type", [
|
||||
noteDataType,
|
||||
contactDataType,
|
||||
]);
|
||||
const getUserImagesResponseValidator = array(dataTypeValidator);
|
||||
|
||||
const userImageValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
UserID: pipe(string(), uuid()),
|
||||
Image: strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageName: string(),
|
||||
Image: null_(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type UserImage = InferOutput<typeof dataTypeValidator>;
|
||||
|
||||
export const getUserImages = async (): Promise<UserImage[]> => {
|
||||
const imageRequestValidator = strictObject({
|
||||
UserImages: array(userImageValidator),
|
||||
ImageProperties: array(dataTypeValidator),
|
||||
});
|
||||
|
||||
export const getUserImages = async (): Promise<
|
||||
InferOutput<typeof imageRequestValidator>
|
||||
> => {
|
||||
const request = getBaseAuthorizedRequest({ path: "image" });
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
|
||||
console.log("BACKEND RESPONSE: ", res);
|
||||
|
||||
return parse(getUserImagesResponseValidator, res);
|
||||
return parse(imageRequestValidator, res);
|
||||
};
|
||||
|
||||
export const getImage = async (imageId: string): Promise<UserImage> => {
|
||||
|
||||
16
frontend/src/utils/groupPropertiesWithImage.ts
Normal file
16
frontend/src/utils/groupPropertiesWithImage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { getUserImages } from "../network";
|
||||
|
||||
export const groupPropertiesWithImage = ({
|
||||
UserImages,
|
||||
ImageProperties,
|
||||
}: Awaited<ReturnType<typeof getUserImages>>) => {
|
||||
const imageToProperties: Record<string, typeof ImageProperties> = {};
|
||||
|
||||
for (const image of UserImages) {
|
||||
imageToProperties[image.ImageID] = ImageProperties.filter((i) =>
|
||||
i.data.Images.includes(image.ImageID),
|
||||
);
|
||||
}
|
||||
|
||||
return imageToProperties;
|
||||
};
|
||||
Reference in New Issue
Block a user