Haystack V2: Removing entities completely

This commit is contained in:
2025-07-29 14:52:33 +01:00
parent 3d05ff708e
commit a0bf27dd16
47 changed files with 16 additions and 3052 deletions

View File

@@ -2,11 +2,9 @@ import { Navigate, Route, Router } from "@solidjs/router";
import { onAndroidMount } from "./mobile";
import {
FrontPage,
Gallery,
ImagePage,
Login,
Settings,
Entity,
SearchPage,
AllImages,
List,
@@ -36,8 +34,6 @@ export const App = () => {
<Route path="/all-images" component={AllImages} />
<Route path="/image/:imageId" component={ImagePage} />
<Route path="/list/:listId" component={List} />
<Route path="/entity/:entityId" component={Entity} />
<Route path="/gallery/:entity" component={Gallery} />
<Route path="/settings" component={Settings} />
</Route>
</Route>

View File

@@ -1,82 +0,0 @@
import type { UserImage } from "../../network";
import { Show, type Component } from "solid-js";
import SolidjsMarkdown from "solidjs-markdown";
type Props = {
item: UserImage;
};
const NullableParagraph: Component<{
item: string | null;
itemTitle: string;
}> = (props) => {
return (
<Show when={props.item}>
{(item) => (
<>
<p class="font-semibold text-xl">{props.itemTitle}</p>
<p class="text-md">{item()}</p>
</>
)}
</Show>
);
};
const ConcreteItemModal: Component<Props> = (props) => {
switch (props.item.type) {
case "note":
return (
<SolidjsMarkdown>
{props.item.data.Content.slice(
"```markdown".length,
props.item.data.Content.length - "```".length,
)}
</SolidjsMarkdown>
);
case "location":
return (
<div class="flex flex-col gap-2">
<p class="font-semibold text-xl">Address</p>
<p class="text-md">{props.item.data.Address}</p>
</div>
);
case "event":
return (
<div class="flex flex-col gap-2">
<p class="font-semibold text-xl">Event</p>
<p class="text-md">{props.item.data.Name}</p>
<NullableParagraph
itemTitle="Start Time"
item={props.item.data.StartDateTime}
/>
<NullableParagraph
itemTitle="End Time"
item={props.item.data.EndDateTime}
/>
</div>
);
case "contact":
return (
<div class="flex flex-col gap-2">
<p class="font-semibold text-xl">Contact</p>
<p class="text-md">{props.item.data.Name}</p>
<NullableParagraph itemTitle="Email" item={props.item.data.Email} />
<NullableParagraph
itemTitle="Phone Number"
item={props.item.data.PhoneNumber}
/>
</div>
);
}
};
export const ItemModal: Component<Props> = (props) => {
return (
<div class="rounded-2xl p-4 bg-white border border-neutral-300 flex flex-col gap-2 mb-2">
<ConcreteItemModal item={props.item} />
</div>
);
};

View File

@@ -1,31 +0,0 @@
import { A } from "@solidjs/router";
import type { UserImage } from "../../network";
import { SearchCardContact } from "./SearchCardContact";
import { SearchCardEvent } from "./SearchCardEvent";
import { SearchCardLocation } from "./SearchCardLocation";
const UnwrappedSearchCard = (props: { item: UserImage }) => {
const { item } = props;
switch (item.type) {
case "location":
return <SearchCardLocation item={item} />;
case "event":
return <SearchCardEvent item={item} />;
case "contact":
return <SearchCardContact item={item} />;
default:
return null;
}
};
export const SearchCard = (props: { item: UserImage }) => {
return (
<A
href={`/entity/${props.item.data.ID}`}
class="w-full h-[144px] border relative border-neutral-200 cursor-pointer overflow-hidden rounded-xl"
>
<UnwrappedSearchCard item={props.item} />
</A>
);
};

View File

@@ -1,24 +0,0 @@
import { IconUser } from "@tabler/icons-solidjs";
import type { UserImage } from "../../network";
type Props = {
item: Extract<UserImage, { type: "contact" }>;
};
export const SearchCardContact = ({ item }: Props) => {
const { data } = item;
return (
<div class="h-full inset-0 p-3 bg-orange-50">
<div class="flex mb-1 items-center gap-1">
<IconUser size={14} class="text-neutral-500" />
<p class="text-xs text-neutral-500">Contact</p>
</div>
<p class="text-sm text-neutral-900 font-bold mb-1">
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
</p>
<p class="text-xs text-neutral-700">Phone: {data.PhoneNumber}</p>
<p class="text-xs text-neutral-700">Mail: {data.Email}</p>
</div>
);
};

View File

@@ -1,32 +0,0 @@
import { IconCalendar } from "@tabler/icons-solidjs";
import type { UserImage } from "../../network";
type Props = {
item: Extract<UserImage, { type: "event" }>;
};
export const SearchCardEvent = ({ item }: Props) => {
const { data } = item;
return (
<div class="h-full inset-0 p-3 bg-purple-50">
<div class="flex mb-1 items-center gap-1">
<IconCalendar size={14} class="text-neutral-500" />
<p class="text-xs text-neutral-500">Event</p>
</div>
<p class="text-sm text-neutral-900 font-bold mb-1">
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
</p>
<p class="text-xs text-neutral-700">
On{" "}
{data.StartDateTime
? new Date(data.StartDateTime).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
: "unknown date"}
</p>
</div>
);
};

View File

@@ -1,23 +0,0 @@
import { IconMapPin } from "@tabler/icons-solidjs";
import type { UserImage } from "../../network";
type Props = {
item: Extract<UserImage, { type: "location" }>;
};
export const SearchCardLocation = ({ item }: Props) => {
const { data } = item;
return (
<div class="h-full inset-0 p-3 bg-red-50">
<div class="flex mb-1 items-center gap-1">
<IconMapPin size={14} class="text-neutral-500" />
<p class="text-xs text-neutral-500">Location</p>
</div>
<p class="text-sm text-neutral-900 font-bold mb-1">
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
</p>
<p class="text-xs text-neutral-700">Address: {data.Address}</p>
</div>
);
};

View File

@@ -7,26 +7,9 @@ import {
createResource,
useContext,
} from "solid-js";
import {
CategoryUnion,
getUserImages,
JustTheImageWhatAreTheseNames,
List,
UserImage,
} from "../network";
import { groupPropertiesWithImage } from "../utils/groupPropertiesWithImage";
type TaggedCategory<T extends CategoryUnion["type"]> = Extract<
CategoryUnion,
{ type: T }
>["data"];
type CategoriesSpecificData = {
[K in CategoryUnion["type"]]: Array<TaggedCategory<K>>;
};
import { getUserImages, JustTheImageWhatAreTheseNames } from "../network";
export type SearchImageStore = {
images: Accessor<UserImage[]>;
imagesByDate: Accessor<
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
>;
@@ -35,13 +18,10 @@ export type SearchImageStore = {
userImages: Accessor<JustTheImageWhatAreTheseNames>;
imagesWithProperties: Accessor<ReturnType<typeof groupPropertiesWithImage>>;
processingImages: Accessor<
Awaited<ReturnType<typeof getUserImages>>["ProcessingImages"] | undefined
>;
categories: Accessor<CategoriesSpecificData>;
onRefetchImages: () => void;
};
@@ -49,15 +29,6 @@ const SearchImageContext = createContext<SearchImageStore>();
export const SearchImageContextProvider: Component<ParentProps> = (props) => {
const [data, { refetch }] = createResource(getUserImages);
const imageData = createMemo(() => {
const d = data();
if (d == null) {
return [];
}
return d.ImageProperties;
});
const sortedImages = createMemo<ReturnType<SearchImageStore["imagesByDate"]>>(
() => {
const d = data();
@@ -89,42 +60,14 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
const processingImages = () => data()?.ProcessingImages ?? [];
const imagesWithProperties = createMemo<
ReturnType<typeof groupPropertiesWithImage>
>(() => {
const d = data();
if (d == null) {
return {};
}
return groupPropertiesWithImage(d);
});
const categories = createMemo(() => {
const c: ReturnType<SearchImageStore["categories"]> = {
contact: [],
event: [],
location: [],
};
for (const category of data()?.ImageProperties ?? []) {
c[category.type].push(category.data as any);
}
return c;
});
return (
<SearchImageContext.Provider
value={{
images: imageData,
imagesByDate: sortedImages,
lists: () => data()?.Lists ?? [],
imagesWithProperties: imagesWithProperties,
userImages: () => data()?.UserImages ?? [],
processingImages,
onRefetchImages: refetch,
categories,
}}
>
{props.children}

View File

@@ -12,7 +12,6 @@ import {
string,
union,
uuid,
variant,
} from "valibot";
type BaseRequestParams = Partial<{
@@ -85,62 +84,6 @@ export const sendImage = async (
return parse(sendImageResponseValidator, res);
};
const locationValidator = strictObject({
ID: pipe(string(), uuid()),
CreatedAt: pipe(string()),
Name: string(),
Address: nullable(string()),
Description: nullable(string()),
Images: array(pipe(string(), uuid())),
});
const contactValidator = strictObject({
ID: pipe(string(), uuid()),
CreatedAt: pipe(string()),
Name: string(),
Description: nullable(string()),
PhoneNumber: nullable(string()),
Email: nullable(string()),
Images: array(pipe(string(), uuid())),
});
const eventValidator = strictObject({
ID: pipe(string(), uuid()),
CreatedAt: nullable(pipe(string())),
Name: string(),
StartDateTime: nullable(pipe(string())),
EndDateTime: nullable(pipe(string())),
Description: nullable(string()),
LocationID: nullable(pipe(string(), uuid())),
// Location: nullable(locationValidator),
OrganizerID: nullable(pipe(string(), uuid())),
// Organizer: nullable(contactValidator),
Images: array(pipe(string(), uuid())),
});
const locationDataType = strictObject({
type: literal("location"),
data: locationValidator,
});
const eventDataType = strictObject({
type: literal("event"),
data: eventValidator,
});
const contactDataType = strictObject({
type: literal("contact"),
data: contactValidator,
});
const dataTypeValidator = variant("type", [
locationDataType,
eventDataType,
contactDataType,
]);
export type CategoryUnion = InferOutput<typeof dataTypeValidator>;
const imageMetaValidator = strictObject({
ID: pipe(string(), uuid()),
ImageName: string(),
@@ -168,8 +111,6 @@ const userProcessingImageValidator = strictObject({
]),
});
export type UserImage = InferOutput<typeof dataTypeValidator>;
const listValidator = strictObject({
ID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()),
@@ -212,7 +153,6 @@ export type List = InferOutput<typeof listValidator>;
const imageRequestValidator = strictObject({
UserImages: array(userImageValidator),
ImageProperties: array(dataTypeValidator),
ProcessingImages: array(userProcessingImageValidator),
Lists: array(listValidator),
});
@@ -233,15 +173,6 @@ export const getUserImages = async (): Promise<
return parse(imageRequestValidator, res);
};
export const getImage = async (imageId: string): Promise<UserImage> => {
const request = getBaseAuthorizedRequest({
path: `image-properties/${imageId}`,
});
const res = await fetch(request).then((res) => res.json());
return parse(dataTypeValidator, res);
};
export const postLogin = async (email: string): Promise<void> => {
const request = getBaseRequest({
path: "login",

View File

@@ -1,28 +0,0 @@
import { ImageComponent } from "@components/image";
import { ItemModal } from "@components/item-modal/ItemModal";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import { useParams } from "@solidjs/router";
import { Component, For, Show } from "solid-js";
export const Entity: Component = () => {
const params = useParams<{ entityId: string }>();
const { images } = useSearchImageContext();
const entity = () => images().find((i) => i.data.ID === params.entityId);
return (
<Show when={entity()} fallback={<>Sorry, this entity could not be found</>}>
{(e) => (
<div>
<ItemModal item={e()} />
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4 bg-white p-4 rounded-xl border border-neutral-200">
<For each={e().data.Images}>
{(imageId) => <ImageComponent ID={imageId} />}
</For>
</div>
</div>
)}
</Show>
);
};

View File

@@ -1,21 +1,8 @@
import { Component, For } from "solid-js";
import { A } from "@solidjs/router";
import {
SearchImageStore,
useSearchImageContext,
} from "@contexts/SearchImageContext";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import fastHashCode from "../../utils/hash";
// TODO: lots of stuff to do with Entities, this could be seperated into a centralized place.
const CategoryColor: Record<
keyof ReturnType<SearchImageStore["categories"]>,
string
> = {
contact: "bg-orange-50",
location: "bg-red-50",
event: "bg-purple-50",
};
const colors = [
"bg-emerald-50",
"bg-lime-50",
@@ -31,30 +18,10 @@ const colors = [
];
export const Categories: Component = () => {
const { categories, lists } = useSearchImageContext();
const { lists } = useSearchImageContext();
return (
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
<h2 class="text-xl font-bold">Entities</h2>
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4">
<For each={Object.entries(categories())}>
{([category, group]) => (
<A
href={`/gallery/${category}`}
class={
"col-span-2 flex flex-col justify-center items-center rounded-lg p-4 border border-neutral-200 " +
"capitalize " +
CategoryColor[category as keyof typeof CategoryColor] +
" " +
(group.length === 0 ? "row-span-1 order-10" : "row-span-2")
}
>
<p class="text-xl font-bold">{category}s</p>
<p class="text-lg">{group.length}</p>
</A>
)}
</For>
</div>
<h2 class="text-xl font-bold">Generated Lists</h2>
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
<For each={lists()}>

View File

@@ -1,52 +0,0 @@
import { Component, For, Show } from "solid-js";
import { useParams } from "@solidjs/router";
import { union, literal, safeParse, InferOutput, parse } from "valibot";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import { SearchCard } from "@components/search-card/SearchCard";
const entityValidator = union([
literal("event"),
literal("note"),
literal("location"),
literal("contact"),
]);
const EntityGallery: Component<{
entity: InferOutput<typeof entityValidator>;
}> = (props) => {
// Just to be doubly sure.
parse(entityValidator, props.entity);
// These names are being silly. Entity or Category?
const { images } = useSearchImageContext();
const filteredCategories = () =>
images().filter((i) => i.type === props.entity);
return (
<div class="w-full flex flex-col gap-4 capitalize bg-white rounded-xl p-4">
<h2 class="font-bold text-xl">
{props.entity}s ({filteredCategories().length})
</h2>
<div class="grid grid-cols-3 gap-4">
<For each={filteredCategories()}>
{(category) => <SearchCard item={category} />}
</For>
</div>
</div>
);
};
export const Gallery: Component = () => {
const params = useParams();
const validated = safeParse(entityValidator, params.entity);
return (
<Show
when={validated.success}
fallback={<p>Sorry, this entity is not supported</p>}
>
<EntityGallery entity={validated.output as any} />
</Show>
);
};

View File

@@ -1,25 +1,16 @@
import { ImageComponent } from "@components/image";
import { SearchCard } from "@components/search-card/SearchCard";
import { useSearchImageContext } from "@contexts/SearchImageContext";
import { UserImage } from "@network/index";
import { useParams } from "@solidjs/router";
import { createEffect, For, Show, type Component } from "solid-js";
import { type Component } from "solid-js";
import SolidjsMarkdown from "solidjs-markdown";
export const ImagePage: Component = () => {
const { imageId } = useParams<{ imageId: string }>();
const { imagesWithProperties, userImages } = useSearchImageContext();
const { userImages } = useSearchImageContext();
const image = () => userImages().find((i) => i.ImageID === imageId);
createEffect(() => {
console.log(userImages());
});
const imageProperties = (): UserImage[] | undefined =>
Object.entries(imagesWithProperties()).find(([id]) => id === imageId)?.[1];
return (
<main class="flex flex-col items-center gap-4">
<div class="w-full bg-white rounded-xl p-4">
@@ -29,15 +20,7 @@ export const ImagePage: Component = () => {
<h2 class="font-bold text-xl">Description</h2>
<SolidjsMarkdown>{image()?.Image.Description}</SolidjsMarkdown>
</div>
<div class="w-full grid grid-cols-3 gap-2 grid-flow-row-dense p-4 bg-white rounded-xl">
<Show when={imageProperties()}>
{(image) => (
<For each={image()}>
{(property) => <SearchCard item={property} />}
</For>
)}
</Show>
</div>
<div class="w-full grid grid-cols-3 gap-2 grid-flow-row-dense p-4 bg-white rounded-xl"></div>
</main>
);
};

View File

@@ -1,9 +1,7 @@
export * from "./front";
export * from "./gallery";
export * from "./image";
export * from "./settings";
export * from "./login";
export * from "./entity";
export * from "./search";
export * from "./all-images";
export * from "./list";

View File

@@ -2,13 +2,13 @@ import { Component, createSignal, For } from "solid-js";
import { Search } from "@kobalte/core/search";
import { IconSearch } from "@tabler/icons-solidjs";
import { useSearch } from "./search";
import { UserImage } from "@network/index";
import { SearchCard } from "@components/search-card/SearchCard";
import { JustTheImageWhatAreTheseNames } from "@network/index";
export const SearchPage: Component = () => {
const fuse = useSearch();
const [searchItems, setSearchItems] = createSignal<UserImage[]>([]);
const [searchItems, setSearchItems] =
createSignal<JustTheImageWhatAreTheseNames>([]);
return (
<Search
@@ -36,7 +36,9 @@ export const SearchPage: Component = () => {
<Search.Portal>
<Search.Content class="container relative w-full rounded-xl bg-white p-4 grid grid-cols-3 gap-4">
<Search.Arrow />
<For each={searchItems()}>{(item) => <SearchCard item={item} />}</For>
<For each={searchItems()}>
{(item) => <div>{item.Image.Description}</div>}
</For>
<Search.NoResult>No result found</Search.NoResult>
</Search.Content>
</Search.Portal>

View File

@@ -1,46 +1,12 @@
import { useSearchImageContext } from "@contexts/SearchImageContext";
import { UserImage } from "@network/index";
import Fuse from "fuse.js";
// This language is stupid. `keyof` only returns common keys but this somehow doesnt.
type KeysOfUnion<T> = T extends T ? keyof T : never;
const weightedTerms: Record<
KeysOfUnion<UserImage["data"]>,
number | undefined
> = {
ID: undefined,
LocationID: undefined,
OrganizerID: undefined,
Images: undefined,
Description: 10,
Name: 5,
Address: 2,
PhoneNumber: 2,
Email: 2,
CreatedAt: 1,
StartDateTime: 1,
EndDateTime: 1,
};
export const useSearch = () => {
const { images, userImages } = useSearchImageContext();
const imageDescriptions = () =>
userImages().map((i) => ({ data: { Description: i.Image.Description } }));
const { userImages } = useSearchImageContext();
return () =>
new Fuse([...images(), ...imageDescriptions()], {
new Fuse(userImages(), {
shouldSort: true,
keys: Object.entries(weightedTerms)
.filter(([, w]) => w != null)
.map(([name, weight]) => ({
name: `data.${name}`,
weight,
})),
keys: ["Image.Description"],
});
};

View File

@@ -1,16 +0,0 @@
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;
};