fix: frontend with new backend schema

This commit is contained in:
2025-10-05 10:44:57 +01:00
parent 1fb9616aa7
commit 649cfe0b02
13 changed files with 111 additions and 142 deletions

View File

@ -34,8 +34,8 @@ type ImageHandler struct {
} }
type ImagesReturn struct { type ImagesReturn struct {
UserImages []models.UserImageWithImage `json:"userImages"` UserImages []models.UserImageWithImage
Lists []models.ListsWithImages `json:"lists"` Stacks []models.ListsWithImages
} }
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) { func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
@ -89,7 +89,7 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
return return
} }
listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId) stacksWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
if err != nil { if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w) middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
return return
@ -97,7 +97,7 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
imagesReturn := ImagesReturn{ imagesReturn := ImagesReturn{
UserImages: images, UserImages: images,
Lists: listsWithImages, Stacks: stacksWithImages,
} }
middleware.WriteJsonOrError(h.logger, imagesReturn, w) middleware.WriteJsonOrError(h.logger, imagesReturn, w)

View File

@ -29,7 +29,7 @@ func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID
} }
func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) { func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
getImageStmt := Image.SELECT(Image.AllColumns.Except(Image.Image)).WHERE(Image.ID.EQ(UUID(imageID))) getImageStmt := Image.SELECT(Image.AllColumns).WHERE(Image.ID.EQ(UUID(imageID)))
image := model.Image{} image := model.Image{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &image) err := getImageStmt.QueryContext(ctx, m.dbPool, &image)

View File

@ -33,14 +33,14 @@ func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
return return
} }
lists, err := h.stackModel.List(ctx, userID) stacks, err := h.stackModel.List(ctx, userID)
if err != nil { if err != nil {
h.logger.Warn("could not get stacks", "err", err) h.logger.Warn("could not get stacks", "err", err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
middleware.WriteJsonOrError(h.logger, lists, w) middleware.WriteJsonOrError(h.logger, stacks, w)
} }
func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) { func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {

View File

@ -4,32 +4,32 @@ import fastHashCode from "../../utils/hash";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
const colors = [ const colors = [
"bg-emerald-50", "bg-emerald-50",
"bg-lime-50", "bg-lime-50",
"bg-indigo-50", "bg-indigo-50",
"bg-sky-50", "bg-sky-50",
"bg-amber-50", "bg-amber-50",
"bg-teal-50", "bg-teal-50",
"bg-fuchsia-50", "bg-fuchsia-50",
"bg-pink-50", "bg-pink-50",
]; ];
export const ListCard: Component<{ list: List }> = (props) => { export const StackCard: Component<{ list: List }> = (props) => {
return ( return (
<A <A
href={`/list/${props.list.ID}`} href={`/list/${props.list.ID}`}
class={ class={
"flex flex-col p-4 border border-neutral-200 rounded-lg " + "flex flex-col p-4 border border-neutral-200 rounded-lg " +
colors[ colors[
fastHashCode(props.list.Name, { forcePositive: true }) % colors.length fastHashCode(props.list.Name, { forcePositive: true }) % colors.length
] ]
} }
> >
<p class="text-xl font-bold">{props.list.Name}</p> <p class="text-xl font-bold">{props.list.Name}</p>
<p class="text-lg">{props.list.Images.length}</p> <p class="text-lg">{props.list.Images.length}</p>
</A> </A>
); );
}; };

View File

@ -34,7 +34,7 @@ export const Notifications = (onCompleteImage: () => void) => {
ProcessingLists: {}, ProcessingLists: {},
}); });
const { processingImages } = useSearchImageContext(); const { userImages } = useSearchImageContext();
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
@ -91,19 +91,19 @@ export const Notifications = (onCompleteImage: () => void) => {
}; };
createEffect(() => { createEffect(() => {
const images = processingImages(); const images = userImages();
if (images == null) { if (images == null) {
return; return;
} }
upsertImageProcessing( upsertImageProcessing(
Object.fromEntries( Object.fromEntries(
images.map((i) => [ images.filter(i => i.Status !== 'complete').map((i) => [
i.ImageID, i.ID,
{ {
Type: "image", Type: "image",
ImageID: i.ImageID, ImageID: i.ID,
ImageName: i.Image.ImageName, ImageName: i.ImageName,
Status: i.Status, Status: i.Status,
}, },
]), ]),

View File

@ -14,14 +14,10 @@ export type SearchImageStore = {
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }> Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
>; >;
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["lists"]>; stacks: Accessor<Awaited<ReturnType<typeof getUserImages>>["Stacks"]>;
userImages: Accessor<JustTheImageWhatAreTheseNames>; userImages: Accessor<JustTheImageWhatAreTheseNames>;
processingImages: Accessor<
Awaited<ReturnType<typeof getUserImages>>["processingImages"] | undefined
>;
onRefetchImages: () => void; onRefetchImages: () => void;
onDeleteImage: (imageID: string) => void; onDeleteImage: (imageID: string) => void;
onDeleteImageFromStack: (stackID: string, imageID: string) => void; onDeleteImageFromStack: (stackID: string, imageID: string) => void;
@ -41,7 +37,7 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
// Sorted by day. But we could potentially add more in the future. // Sorted by day. But we could potentially add more in the future.
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {}; const buckets: Record<string, JustTheImageWhatAreTheseNames> = {};
for (const image of d.userImages) { for (const image of d.UserImages) {
if (image.CreatedAt == null) { if (image.CreatedAt == null) {
continue; continue;
} }
@ -60,15 +56,12 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
}, },
); );
const processingImages = () => data()?.processingImages ?? [];
return ( return (
<SearchImageContext.Provider <SearchImageContext.Provider
value={{ value={{
imagesByDate: sortedImages, imagesByDate: sortedImages,
lists: () => data()?.lists ?? [], stacks: () => data()?.Stacks ?? [],
userImages: () => data()?.userImages ?? [], userImages: () => data()?.UserImages ?? [],
processingImages,
onRefetchImages: refetch, onRefetchImages: refetch,
onDeleteImage: (imageID: string) => { onDeleteImage: (imageID: string) => {
deleteImage(imageID).then(refetch); deleteImage(imageID).then(refetch);

View File

@ -1,12 +1,11 @@
import { getTokenProperties } from "@components/protected-route"; import { getTokenProperties } from "@components/protected-route";
import { fetch } from "@tauri-apps/plugin-http"; import { fetch } from "@tauri-apps/plugin-http";
import { jwtDecode } from "jwt-decode";
import { import {
type InferOutput, type InferOutput,
array, array,
literal,
null_, null_,
literal,
nullable, nullable,
parse, parse,
pipe, pipe,
@ -172,91 +171,70 @@ export const sendImage = async (
return parsedRes.output; return parsedRes.output;
}; };
const imageMetaValidator = strictObject({
ID: pipe(string(), uuid()),
ImageName: string(),
Description: string(),
Image: null_(),
});
const userImageValidator = strictObject({ const userImageValidator = strictObject({
ID: pipe(string(), uuid()), ID: pipe(string(), uuid()),
CreatedAt: pipe(string()), CreatedAt: string(),
ImageID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()), UserID: pipe(string(), uuid()),
Image: strictObject({ Description: string(),
...imageMetaValidator.entries,
ImageLists: pipe(nullable(array( Image: null_(),
strictObject({ ImageName: string(),
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()), Status: union([literal('not-started'), literal('in-progress'), literal('complete')]),
ListID: pipe(string(), uuid()),
}), ImageStacks: pipe(nullable(array(
)), transform(l => l ?? [])), strictObject({
}), ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
}),
)), transform(l => l ?? [])),
}); });
const userProcessingImageValidator = strictObject({ const stackItem = strictObject({
ID: pipe(string(), uuid()), ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()), ImageID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()), SchemaItemID: pipe(string(), uuid()),
Image: imageMetaValidator,
Status: union([ Value: string(),
literal("not-started"), })
literal("in-progress"),
literal("complete"), const stackImage = strictObject({
]), ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
StackID: pipe(string(), uuid()),
Items: pipe(nullable(array(stackItem)), transform(l => l ?? [])),
}); });
const listValidator = strictObject({ const stackSchemaItem = strictObject({
ID: pipe(string(), uuid()), ID: pipe(string(), uuid()),
StackID: pipe(string(), uuid()),
Description: string(),
Item: string(),
Value: string(),
})
const stackValidator = strictObject({
ID: pipe(string(), uuid()),
CreatedAt: string(),
UserID: pipe(string(), uuid()), UserID: pipe(string(), uuid()),
CreatedAt: pipe(string()), Description: string(),
Status: union([literal('not-started'), literal('in-progress'), literal('complete')]),
Name: string(), Name: string(),
Description: nullable(string()),
Images: pipe( Images: array(stackImage),
nullable( SchemaItems: array(stackSchemaItem),
array(
strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
Items: array(
strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
SchemaItemID: pipe(string(), uuid()),
Value: string(),
}),
),
}),
),
),
transform((n) => n ?? []),
),
Schema: strictObject({
ID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
SchemaItems: array(
strictObject({
ID: pipe(string(), uuid()),
SchemaID: pipe(string(), uuid()),
Item: string(),
Value: nullable(string()),
Description: string(),
}),
),
}),
}); });
export type List = InferOutput<typeof listValidator>; export type List = InferOutput<typeof stackValidator>;
const imageRequestValidator = strictObject({ const imageRequestValidator = strictObject({
userImages: array(userImageValidator), UserImages: array(userImageValidator),
processingImages: array(userProcessingImageValidator), Stacks: array(stackValidator),
lists: array(listValidator),
}); });
export type JustTheImageWhatAreTheseNames = InferOutput< export type JustTheImageWhatAreTheseNames = InferOutput<
@ -274,7 +252,7 @@ export const getUserImages = async (): Promise<
const parsedRes = safeParse(imageRequestValidator, res); const parsedRes = safeParse(imageRequestValidator, res);
if (!parsedRes.success) { if (!parsedRes.success) {
console.log(parsedRes.issues) console.log("Schema error: ", parsedRes.issues)
throw new Error(JSON.stringify(parsedRes.issues)); throw new Error(JSON.stringify(parsedRes.issues));
} }
@ -310,7 +288,7 @@ export const postCode = async (
const parsedRes = safeParse(codeValidator, res); const parsedRes = safeParse(codeValidator, res);
if (!parsedRes.success) { if (!parsedRes.success) {
console.log(parsedRes.issues) console.log("Schema error: ", parsedRes.issues)
throw new Error(JSON.stringify(parsedRes.issues)); throw new Error(JSON.stringify(parsedRes.issues));
} }

View File

@ -3,7 +3,6 @@ import { Component, For } from "solid-js";
import { createVirtualizer } from "@tanstack/solid-virtual"; import { createVirtualizer } from "@tanstack/solid-virtual";
import { ImageComponent } from "@components/image"; import { ImageComponent } from "@components/image";
import { chunkRows } from "./chunk"; import { chunkRows } from "./chunk";
import { deleteImage } from "@network/index";
type ImageOrDate = type ImageOrDate =
| { type: "image"; ID: string[] } | { type: "image"; ID: string[] }
@ -21,7 +20,7 @@ export const AllImages: Component = () => {
items.push({ type: "date", date }); items.push({ type: "date", date });
const chunkedRows = chunkRows(3, images); const chunkedRows = chunkRows(3, images);
for (const chunk of chunkedRows) { for (const chunk of chunkedRows) {
items.push({ type: "image", ID: chunk.map((c) => c.ImageID) }); items.push({ type: "image", ID: chunk.map((c) => c.ID) });
} }
} }

View File

@ -1,13 +1,13 @@
import { Component, For, createSignal } from "solid-js"; import { Component, For, createSignal } from "solid-js";
import { useSearchImageContext } from "@contexts/SearchImageContext"; import { useSearchImageContext } from "@contexts/SearchImageContext";
import { ListCard } from "@components/list-card"; import { StackCard } from "@components/list-card";
import { Button } from "@kobalte/core/button"; import { Button } from "@kobalte/core/button";
import { Dialog } from "@kobalte/core/dialog"; import { Dialog } from "@kobalte/core/dialog";
import { createList, ReachedListLimit } from "../../network"; import { createList, ReachedListLimit } from "../../network";
import { createToast } from "../../utils/show-toast"; import { createToast } from "../../utils/show-toast";
export const Categories: Component = () => { export const Categories: Component = () => {
const { lists, onRefetchImages } = useSearchImageContext(); const { stacks, onRefetchImages } = useSearchImageContext();
const [title, setTitle] = createSignal(""); const [title, setTitle] = createSignal("");
const [description, setDescription] = createSignal(""); const [description, setDescription] = createSignal("");
@ -25,11 +25,11 @@ export const Categories: Component = () => {
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setShowForm(false); setShowForm(false);
onRefetchImages(); // Refresh the lists onRefetchImages(); // Refresh the stacks
} catch (error) { } catch (error) {
console.error("Failed to create list:", error); console.error("Failed to create list:", error);
if (error instanceof ReachedListLimit) { if (error instanceof ReachedListLimit) {
createToast("Reached limit!", "You've reached your limit for new lists"); createToast("Reached limit!", "You've reached your limit for new stacks");
} }
} finally { } finally {
setIsCreating(false); setIsCreating(false);
@ -38,9 +38,9 @@ export const Categories: Component = () => {
return ( return (
<div class="rounded-xl bg-white p-4 flex flex-col gap-2"> <div class="rounded-xl bg-white p-4 flex flex-col gap-2">
<h2 class="text-xl font-bold">Generated Lists</h2> <h2 class="text-xl font-bold">Generated stacks</h2>
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4"> <div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
<For each={lists()}>{(list) => <ListCard list={list} />}</For> <For each={stacks()}>{(list) => <StackCard list={list} />}</For>
</div> </div>
<div class="mt-4"> <div class="mt-4">

View File

@ -1,7 +1,6 @@
import { Component, For } from "solid-js"; import { Component, For } from "solid-js";
import { ImageComponent } from "@components/image"; import { ImageComponent } from "@components/image";
import { useSearchImageContext } from "@contexts/SearchImageContext"; import { useSearchImageContext } from "@contexts/SearchImageContext";
import { deleteImage } from "@network/index";
const NUMBER_OF_MAX_RECENT_IMAGES = 10; const NUMBER_OF_MAX_RECENT_IMAGES = 10;
@ -21,7 +20,7 @@ export const Recent: Component = () => {
<h2 class="text-xl font-bold">Recent Screenshots</h2> <h2 class="text-xl font-bold">Recent Screenshots</h2>
<div class="grid grid-cols-3 gap-4 place-items-center"> <div class="grid grid-cols-3 gap-4 place-items-center">
<For each={latestImages()}> <For each={latestImages()}>
{(image) => <ImageComponent ID={image.ImageID} onDelete={onDeleteImage} />} {(image) => <ImageComponent ID={image.ID} onDelete={onDeleteImage} />}
</For> </For>
</div> </div>
</div> </div>

View File

@ -3,15 +3,15 @@ import { useSearchImageContext } from "@contexts/SearchImageContext";
import { useNavigate, useParams } from "@solidjs/router"; import { useNavigate, useParams } from "@solidjs/router";
import { For, type Component } from "solid-js"; import { For, type Component } from "solid-js";
import SolidjsMarkdown from "solidjs-markdown"; import SolidjsMarkdown from "solidjs-markdown";
import { ListCard } from "@components/list-card"; import { StackCard } from "@components/list-card";
export const ImagePage: Component = () => { export const ImagePage: Component = () => {
const { imageId } = useParams<{ imageId: string }>(); const { imageId } = useParams<{ imageId: string }>();
const nav = useNavigate(); const nav = useNavigate();
const { userImages, lists, onDeleteImage } = useSearchImageContext(); const { userImages, stacks, onDeleteImage } = useSearchImageContext();
const image = () => userImages().find((i) => i.ImageID === imageId); const image = () => userImages().find((i) => i.ID === imageId);
return ( return (
<main class="flex flex-col items-center gap-4"> <main class="flex flex-col items-center gap-4">
@ -24,10 +24,10 @@ export const ImagePage: Component = () => {
<div class="w-full bg-white rounded-xl p-4 flex flex-col gap-4"> <div class="w-full bg-white rounded-xl p-4 flex flex-col gap-4">
<h2 class="font-bold text-2xl">Description</h2> <h2 class="font-bold text-2xl">Description</h2>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<For each={image()?.Image.ImageLists}> <For each={image()?.ImageStacks}>
{(imageList) => ( {(imageList) => (
<ListCard <StackCard
list={lists().find((l) => l.ID === imageList.ListID)!} list={stacks().find((l) => l.ID === imageList.ListID)!}
/> />
)} )}
</For> </For>
@ -35,7 +35,7 @@ export const ImagePage: Component = () => {
</div> </div>
<div class="w-full bg-white rounded-xl p-4"> <div class="w-full bg-white rounded-xl p-4">
<h2 class="font-bold text-2xl">Description</h2> <h2 class="font-bold text-2xl">Description</h2>
<SolidjsMarkdown>{image()?.Image.Description}</SolidjsMarkdown> <SolidjsMarkdown>{image()?.Description}</SolidjsMarkdown>
</div> </div>
</main> </main>
); );

View File

@ -107,11 +107,11 @@ export const List: Component = () => {
const { listId } = useParams(); const { listId } = useParams();
const nav = useNavigate(); const nav = useNavigate();
const { lists, onDeleteImageFromStack } = useSearchImageContext(); const { stacks, onDeleteImageFromStack } = useSearchImageContext();
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
const list = () => lists().find((l) => l.ID === listId); const list = () => stacks().find((l) => l.ID === listId);
const handleDeleteList = async () => { const handleDeleteList = async () => {
await deleteList(listId) await deleteList(listId)
@ -148,11 +148,11 @@ export const List: Component = () => {
<th class="px-6 py-4 text-left text-sm font-semibold text-neutral-900 border-r border-neutral-200 min-w-40"> <th class="px-6 py-4 text-left text-sm font-semibold text-neutral-900 border-r border-neutral-200 min-w-40">
Image Image
</th> </th>
<For each={l().Schema.SchemaItems}> <For each={l().SchemaItems}>
{(item, index) => ( {(item, index) => (
<th <th
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${index() < class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${index() <
l().Schema.SchemaItems l().SchemaItems
.length - .length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"

View File

@ -2,7 +2,7 @@ import { Component, createSignal, For } from "solid-js";
import { Search } from "@kobalte/core/search"; import { Search } from "@kobalte/core/search";
import { IconSearch } from "@tabler/icons-solidjs"; import { IconSearch } from "@tabler/icons-solidjs";
import { useSearch } from "./search"; import { useSearch } from "./search";
import { deleteImage, JustTheImageWhatAreTheseNames } from "@network/index"; import { JustTheImageWhatAreTheseNames } from "@network/index";
import { ImageComponent } from "@components/image"; import { ImageComponent } from "@components/image";
import { useSearchImageContext } from "@contexts/SearchImageContext"; import { useSearchImageContext } from "@contexts/SearchImageContext";
@ -41,7 +41,7 @@ export const SearchPage: Component = () => {
<Search.Content class="container relative w-full rounded-xl bg-white p-4 grid grid-cols-3 gap-4"> <Search.Content class="container relative w-full rounded-xl bg-white p-4 grid grid-cols-3 gap-4">
<Search.Arrow /> <Search.Arrow />
<For each={searchItems()}> <For each={searchItems()}>
{(item) => <ImageComponent ID={item.ImageID} onDelete={onDeleteImage} />} {(item) => <ImageComponent ID={item.ID} onDelete={onDeleteImage} />}
</For> </For>
<Search.NoResult>No result found</Search.NoResult> <Search.NoResult>No result found</Search.NoResult>
</Search.Content> </Search.Content>