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 {
UserImages []models.UserImageWithImage `json:"userImages"`
Lists []models.ListsWithImages `json:"lists"`
UserImages []models.UserImageWithImage
Stacks []models.ListsWithImages
}
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
}
listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
stacksWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
return
@ -97,7 +97,7 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
imagesReturn := ImagesReturn{
UserImages: images,
Lists: listsWithImages,
Stacks: stacksWithImages,
}
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) {
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{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)

View File

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

View File

@ -17,7 +17,7 @@ const colors = [
"bg-pink-50",
];
export const ListCard: Component<{ list: List }> = (props) => {
export const StackCard: Component<{ list: List }> = (props) => {
return (
<A
href={`/list/${props.list.ID}`}

View File

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

View File

@ -14,14 +14,10 @@ export type SearchImageStore = {
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
>;
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["lists"]>;
stacks: Accessor<Awaited<ReturnType<typeof getUserImages>>["Stacks"]>;
userImages: Accessor<JustTheImageWhatAreTheseNames>;
processingImages: Accessor<
Awaited<ReturnType<typeof getUserImages>>["processingImages"] | undefined
>;
onRefetchImages: () => void;
onDeleteImage: (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.
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {};
for (const image of d.userImages) {
for (const image of d.UserImages) {
if (image.CreatedAt == null) {
continue;
}
@ -60,15 +56,12 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
},
);
const processingImages = () => data()?.processingImages ?? [];
return (
<SearchImageContext.Provider
value={{
imagesByDate: sortedImages,
lists: () => data()?.lists ?? [],
userImages: () => data()?.userImages ?? [],
processingImages,
stacks: () => data()?.Stacks ?? [],
userImages: () => data()?.UserImages ?? [],
onRefetchImages: refetch,
onDeleteImage: (imageID: string) => {
deleteImage(imageID).then(refetch);

View File

@ -1,12 +1,11 @@
import { getTokenProperties } from "@components/protected-route";
import { fetch } from "@tauri-apps/plugin-http";
import { jwtDecode } from "jwt-decode";
import {
type InferOutput,
array,
literal,
null_,
literal,
nullable,
parse,
pipe,
@ -172,91 +171,70 @@ export const sendImage = async (
return parsedRes.output;
};
const imageMetaValidator = strictObject({
ID: pipe(string(), uuid()),
ImageName: string(),
Description: string(),
Image: null_(),
});
const userImageValidator = strictObject({
ID: pipe(string(), uuid()),
CreatedAt: pipe(string()),
ImageID: pipe(string(), uuid()),
CreatedAt: string(),
UserID: pipe(string(), uuid()),
Image: strictObject({
...imageMetaValidator.entries,
ImageLists: pipe(nullable(array(
Description: string(),
Image: null_(),
ImageName: string(),
Status: union([literal('not-started'), literal('in-progress'), literal('complete')]),
ImageStacks: pipe(nullable(array(
strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
}),
)), transform(l => l ?? [])),
}),
});
const userProcessingImageValidator = strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()),
Image: imageMetaValidator,
Status: union([
literal("not-started"),
literal("in-progress"),
literal("complete"),
]),
});
const listValidator = strictObject({
ID: pipe(string(), uuid()),
UserID: pipe(string(), uuid()),
CreatedAt: pipe(string()),
Name: string(),
Description: nullable(string()),
Images: pipe(
nullable(
array(
strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
ListID: pipe(string(), uuid()),
Items: array(
strictObject({
const stackItem = strictObject({
ID: pipe(string(), uuid()),
ImageID: pipe(string(), uuid()),
SchemaItemID: pipe(string(), uuid()),
Value: string(),
}),
),
}),
),
),
transform((n) => n ?? []),
),
Schema: strictObject({
Value: string(),
})
const stackImage = 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(),
}),
),
}),
ImageID: pipe(string(), uuid()),
StackID: pipe(string(), uuid()),
Items: pipe(nullable(array(stackItem)), transform(l => l ?? [])),
});
export type List = InferOutput<typeof listValidator>;
const stackSchemaItem = strictObject({
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()),
Description: string(),
Status: union([literal('not-started'), literal('in-progress'), literal('complete')]),
Name: string(),
Images: array(stackImage),
SchemaItems: array(stackSchemaItem),
});
export type List = InferOutput<typeof stackValidator>;
const imageRequestValidator = strictObject({
userImages: array(userImageValidator),
processingImages: array(userProcessingImageValidator),
lists: array(listValidator),
UserImages: array(userImageValidator),
Stacks: array(stackValidator),
});
export type JustTheImageWhatAreTheseNames = InferOutput<
@ -274,7 +252,7 @@ export const getUserImages = async (): Promise<
const parsedRes = safeParse(imageRequestValidator, res);
if (!parsedRes.success) {
console.log(parsedRes.issues)
console.log("Schema error: ", parsedRes.issues)
throw new Error(JSON.stringify(parsedRes.issues));
}
@ -310,7 +288,7 @@ export const postCode = async (
const parsedRes = safeParse(codeValidator, res);
if (!parsedRes.success) {
console.log(parsedRes.issues)
console.log("Schema error: ", 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 { ImageComponent } from "@components/image";
import { chunkRows } from "./chunk";
import { deleteImage } from "@network/index";
type ImageOrDate =
| { type: "image"; ID: string[] }
@ -21,7 +20,7 @@ export const AllImages: Component = () => {
items.push({ type: "date", date });
const chunkedRows = chunkRows(3, images);
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 { useSearchImageContext } from "@contexts/SearchImageContext";
import { ListCard } from "@components/list-card";
import { StackCard } from "@components/list-card";
import { Button } from "@kobalte/core/button";
import { Dialog } from "@kobalte/core/dialog";
import { createList, ReachedListLimit } from "../../network";
import { createToast } from "../../utils/show-toast";
export const Categories: Component = () => {
const { lists, onRefetchImages } = useSearchImageContext();
const { stacks, onRefetchImages } = useSearchImageContext();
const [title, setTitle] = createSignal("");
const [description, setDescription] = createSignal("");
@ -25,11 +25,11 @@ export const Categories: Component = () => {
setTitle("");
setDescription("");
setShowForm(false);
onRefetchImages(); // Refresh the lists
onRefetchImages(); // Refresh the stacks
} catch (error) {
console.error("Failed to create list:", error);
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 {
setIsCreating(false);
@ -38,9 +38,9 @@ export const Categories: Component = () => {
return (
<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">
<For each={lists()}>{(list) => <ListCard list={list} />}</For>
<For each={stacks()}>{(list) => <StackCard list={list} />}</For>
</div>
<div class="mt-4">

View File

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

View File

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

View File

@ -107,11 +107,11 @@ export const List: Component = () => {
const { listId } = useParams();
const nav = useNavigate();
const { lists, onDeleteImageFromStack } = useSearchImageContext();
const { stacks, onDeleteImageFromStack } = useSearchImageContext();
const [accessToken] = createResource(getAccessToken);
const list = () => lists().find((l) => l.ID === listId);
const list = () => stacks().find((l) => l.ID === listId);
const handleDeleteList = async () => {
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">
Image
</th>
<For each={l().Schema.SchemaItems}>
<For each={l().SchemaItems}>
{(item, index) => (
<th
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${index() <
l().Schema.SchemaItems
l().SchemaItems
.length -
1
? "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 { IconSearch } from "@tabler/icons-solidjs";
import { useSearch } from "./search";
import { deleteImage, JustTheImageWhatAreTheseNames } from "@network/index";
import { JustTheImageWhatAreTheseNames } from "@network/index";
import { ImageComponent } from "@components/image";
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.Arrow />
<For each={searchItems()}>
{(item) => <ImageComponent ID={item.ImageID} onDelete={onDeleteImage} />}
{(item) => <ImageComponent ID={item.ID} onDelete={onDeleteImage} />}
</For>
<Search.NoResult>No result found</Search.NoResult>
</Search.Content>