fix: stuff

This commit is contained in:
2025-09-21 16:48:17 +01:00
parent f6393c9a59
commit f8619d3ef7
5 changed files with 196 additions and 88 deletions

View File

@ -9,6 +9,7 @@ import (
. "screenmark/screenmark/.gen/haystack/haystack/table" . "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres" . "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -250,11 +251,15 @@ func (m ImageModel) Delete(ctx context.Context, imageID uuid.UUID) error {
func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool { func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool {
getImageUserId := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId))) getImageUserId := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
getProcessingImageUserId := UserImagesToProcess.SELECT(UserImagesToProcess.UserID).WHERE(UserImagesToProcess.ImageID.EQ(UUID(imageId)))
userImage := model.UserImages{} userImage := model.UserImages{}
err := getImageUserId.QueryContext(ctx, m.dbPool, &userImage) userProcessingImage := model.UserImagesToProcess{}
return err == nil && userImage.UserID.String() == userId.String() err1 := getImageUserId.QueryContext(ctx, m.dbPool, &userImage)
err2 := getProcessingImageUserId.QueryContext(ctx, m.dbPool, &userProcessingImage)
return (err1 == nil || err1 == qrm.ErrNoRows) && (err2 == nil || err2 == qrm.ErrNoRows) && (userImage.UserID.String() == userId.String() || userProcessingImage.UserID.String() == userId.String())
} }
func NewImageModel(db *sql.DB) ImageModel { func NewImageModel(db *sql.DB) ImageModel {

View File

@ -68,13 +68,13 @@ CREATE TABLE haystack.image_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE, image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
list_id UUID NOT NULL REFERENCES haystack.lists (id) list_id UUID NOT NULL REFERENCES haystack.lists (id) ON DELETE CASCADE
); );
CREATE TABLE haystack.schemas ( CREATE TABLE haystack.schemas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
list_id UUID NOT NULL REFERENCES haystack.lists (id) list_id UUID NOT NULL REFERENCES haystack.lists (id) ON DELETE CASCADE
); );
CREATE TABLE haystack.schema_items ( CREATE TABLE haystack.schema_items (

View File

@ -1,82 +1,86 @@
import { Popover } from "@kobalte/core/popover"; import { Popover } from "@kobalte/core/popover";
import { Component, For, Show } from "solid-js"; import { Component, createResource, For, Show, Suspense } from "solid-js";
import { LoadingCircle } from "./LoadingCircle"; import { LoadingCircle } from "./LoadingCircle";
import { base } from "@network/index"; import { base, getAccessToken } from "@network/index";
import { useNotifications } from "@contexts/Notifications"; import { useNotifications } from "@contexts/Notifications";
export const ProcessingImages: Component = () => { export const ProcessingImages: Component = () => {
const notifications = useNotifications(); const notifications = useNotifications();
const processingNumber = () => const processingNumber = () =>
Object.keys(notifications.state.ProcessingImages).length + Object.keys(notifications.state.ProcessingImages).length +
Object.keys(notifications.state.ProcessingLists).length; Object.keys(notifications.state.ProcessingLists).length;
return ( const [accessToken] = createResource(getAccessToken)
<Popover sameWidth gutter={4}>
<Popover.Trigger class="w-full flex justify-between gap-4 rounded-xl px-4 py-2">
<Show when={processingNumber() > 0}>
<p class="text-md">
Processing {processingNumber()}{" "}
{processingNumber() === 1 ? "item" : "items"}
...
</p>
</Show>
<Show
when={processingNumber() === 0}
fallback={<LoadingCircle status="loading" />}
>
<LoadingCircle status="complete" />
</Show>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content class="shadow-2xl flex flex-col gap-2 bg-white rounded-xl p-2">
<Show
when={processingNumber() > 0}
fallback={<p>No items to process</p>}
>
<For each={Object.entries(notifications.state.ProcessingImages)}>
{([id, _image]) => (
<Show when={_image}>
{(image) => (
<div class="flex gap-2 w-full justify-center">
<img
class="w-16 h-16 aspect-square rounded"
alt="processing"
src={`${base}/images/${id}`}
/>
<div class="flex flex-col gap-1">
<p class="text-slate-100">{image().ImageName}</p>
</div>
<LoadingCircle
status="loading"
class="ml-auto self-center"
/>
</div>
)}
</Show>
)}
</For>
<For each={Object.entries(notifications.state.ProcessingLists)}> return (
{([, _list]) => ( <Suspense>
<Show when={_list}> <Popover sameWidth gutter={4}>
{(list) => ( <Popover.Trigger class="w-full flex justify-between gap-4 rounded-xl px-4 py-2">
<div class="flex gap-2 w-full justify-center"> <Show when={processingNumber() > 0}>
<div class="flex flex-col gap-1"> <p class="text-md">
<p class="text-slate-900">New Stack: {list().Name}</p> Processing {processingNumber()}{" "}
</div> {processingNumber() === 1 ? "item" : "items"}
<LoadingCircle ...
status="loading" </p>
class="ml-auto self-center" </Show>
/> <Show
</div> when={processingNumber() === 0}
)} fallback={<LoadingCircle status="loading" />}
</Show> >
)} <LoadingCircle status="complete" />
</For> </Show>
</Show> </Popover.Trigger>
</Popover.Content> <Popover.Portal>
</Popover.Portal> <Popover.Content class="shadow-2xl flex flex-col gap-2 bg-white rounded-xl p-2">
</Popover> <Show
); when={processingNumber() > 0}
fallback={<p>No items to process</p>}
>
<For each={Object.entries(notifications.state.ProcessingImages)}>
{([id, _image]) => (
<Show when={_image}>
{(image) => (
<div class="flex gap-2 w-full justify-center">
<img
class="w-16 h-16 aspect-square rounded"
alt="processing"
src={`${base}/images/${id}?token=${accessToken()}`}
/>
<div class="flex flex-col gap-1">
<p class="text-slate-100">{image().ImageName}</p>
</div>
<LoadingCircle
status="loading"
class="ml-auto self-center"
/>
</div>
)}
</Show>
)}
</For>
<For each={Object.entries(notifications.state.ProcessingLists)}>
{([, _list]) => (
<Show when={_list}>
{(list) => (
<div class="flex gap-2 w-full justify-center">
<div class="flex flex-col gap-1">
<p class="text-slate-900">New Stack: {list().Name}</p>
</div>
<LoadingCircle
status="loading"
class="ml-auto self-center"
/>
</div>
)}
</Show>
)}
</For>
</Show>
</Popover.Content>
</Popover.Portal>
</Popover>
</Suspense>
);
}; };

View File

@ -46,7 +46,7 @@ export const getAccessToken = async (): Promise<string> => {
throw new Error("your are not logged in") throw new Error("your are not logged in")
} }
const isValidAccessToken = accessToken != null && getTokenProperties(accessToken).exp.getTime() > Date.now() const isValidAccessToken = accessToken != null && getTokenProperties(accessToken).exp.getTime() * 1000 > Date.now()
if (!isValidAccessToken) { if (!isValidAccessToken) {
const newAccessToken = await fetch(getBaseRequest({ const newAccessToken = await fetch(getBaseRequest({
@ -129,6 +129,15 @@ export const deleteImageFromStack = async (listID: string, imageID: string): Pro
await fetch(request); await fetch(request);
} }
export const deleteList = async (listID: string): Promise<void> => {
const request = await getBaseAuthorizedRequest({
path: `stacks/${listID}`,
method: "DELETE",
});
await fetch(request);
}
export class ImageLimitReached extends Error { export class ImageLimitReached extends Error {
constructor() { constructor() {
super(); super();

View File

@ -1,7 +1,14 @@
import { useSearchImageContext } from "@contexts/SearchImageContext"; import { useSearchImageContext } from "@contexts/SearchImageContext";
import { useParams } from "@solidjs/router"; import { useParams, useNavigate } from "@solidjs/router";
import { Component, For, Show, Suspense, createResource, createSignal } from "solid-js"; import {
import { base, getAccessToken } from "../../network"; Component,
For,
Show,
Suspense,
createResource,
createSignal,
} from "solid-js";
import { base, deleteList, getAccessToken } from "../../network";
import { Dialog } from "@kobalte/core"; import { Dialog } from "@kobalte/core";
const DeleteButton: Component<{ onDelete: () => void }> = (props) => { const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
@ -35,7 +42,10 @@ const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
Cancel Cancel
</button> </button>
</Dialog.CloseButton> </Dialog.CloseButton>
<button class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" onClick={props.onDelete}> <button
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={props.onDelete}
>
Confirm Confirm
</button> </button>
</div> </div>
@ -47,8 +57,55 @@ const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
); );
}; };
const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
const [isOpen, setIsOpen] = createSignal(false);
return (
<>
<button
aria-label="Delete list"
class="text-white bg-red-600 hover:bg-red-700 rounded px-3 py-2 text-sm font-medium"
onClick={() => setIsOpen(true)}
>
Delete List
</button>
<Dialog.Root open={isOpen()} onOpenChange={setIsOpen}>
<Dialog.Portal>
<Dialog.Overlay class="fixed inset-0 bg-black bg-opacity-50" />
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<Dialog.Title class="text-lg font-bold mb-2">
Confirm Delete List
</Dialog.Title>
<Dialog.Description class="mb-4">
Are you sure you want to delete this entire
list? This action cannot be undone.
</Dialog.Description>
<div class="flex justify-end gap-2">
<Dialog.CloseButton>
<button class="px-4 py-2 bg-gray-300 rounded hover:bg-gray-400">
Cancel
</button>
</Dialog.CloseButton>
<button
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={props.onDelete}
>
Delete List
</button>
</div>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
</>
);
};
export const List: Component = () => { export const List: Component = () => {
const { listId } = useParams(); const { listId } = useParams();
const nav = useNavigate();
const { lists, onDeleteImageFromStack } = useSearchImageContext(); const { lists, onDeleteImageFromStack } = useSearchImageContext();
@ -56,12 +113,35 @@ export const List: Component = () => {
const list = () => lists().find((l) => l.ID === listId); const list = () => lists().find((l) => l.ID === listId);
const handleDeleteList = async () => {
await deleteList(listId)
nav("/");
};
return ( return (
<Suspense> <Suspense>
<Show when={list()} fallback="List could not be found"> <Show when={list()} fallback="List could not be found">
{(l) => ( {(l) => (
<div class="w-full h-full bg-white rounded-lg shadow-sm border border-neutral-200 overflow-hidden"> <div class="w-full h-full bg-white rounded-lg shadow-sm border border-neutral-200 overflow-hidden">
<div class="overflow-x-auto overflow-y-auto h-full"> <div class="px-6 py-4 border-b border-neutral-200 bg-neutral-50">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-semibold text-neutral-900">
{l().Name}
</h1>
<Show when={l().Description}>
<p class="text-sm text-neutral-600 mt-1">
{l().Description}
</p>
</Show>
</div>
<DeleteListButton onDelete={handleDeleteList} />
</div>
</div>
<div
class="overflow-x-auto overflow-y-auto"
style="height: calc(100% - 80px);"
>
<table class="w-full min-w-full"> <table class="w-full min-w-full">
<thead class="bg-neutral-50 border-b border-neutral-200 sticky top-0 z-10"> <thead class="bg-neutral-50 border-b border-neutral-200 sticky top-0 z-10">
<tr> <tr>
@ -106,14 +186,22 @@ export const List: Component = () => {
alt="List item" alt="List item"
/> />
</a> </a>
<DeleteButton onDelete={() => onDeleteImageFromStack(l().ID, image.ImageID)} /> <DeleteButton
onDelete={() =>
onDeleteImageFromStack(
l().ID,
image.ImageID,
)
}
/>
</div> </div>
</td> </td>
<For each={image.Items}> <For each={image.Items}>
{(item, colIndex) => ( {(item, colIndex) => (
<td <td
class={`px-6 py-4 text-sm text-neutral-700 ${colIndex() < class={`px-6 py-4 text-sm text-neutral-700 ${colIndex() <
image.Items.length - image.Items
.length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
: "" : ""
@ -121,7 +209,9 @@ export const List: Component = () => {
> >
<div <div
class="max-w-xs truncate" class="max-w-xs truncate"
title={item.Value} title={
item.Value
}
> >
{item.Value} {item.Value}
</div> </div>
@ -139,8 +229,8 @@ export const List: Component = () => {
No images in this list yet No images in this list yet
</p> </p>
<p class="text-sm mt-1"> <p class="text-sm mt-1">
Images will appear here once added to the Images will appear here once added to
list the list
</p> </p>
</div> </div>
</Show> </Show>