refactor: list -> stack

This commit is contained in:
2025-10-05 15:12:37 +01:00
parent 9948d2521b
commit 838ab37fc1
8 changed files with 59 additions and 59 deletions

View File

@ -53,7 +53,7 @@ func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
return return
} }
stackID, err := middleware.GetPathParamID(h.logger, "listID", w, r) stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
if err != nil { if err != nil {
return return
} }
@ -86,7 +86,7 @@ func (h *StackHandler) deleteStack(w http.ResponseWriter, r *http.Request) {
return return
} }
stackID, err := middleware.GetPathParamID(h.logger, "listID", w, r) stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
if err != nil { if err != nil {
return return
} }

View File

@ -7,7 +7,7 @@ import {
Settings, Settings,
SearchPage, SearchPage,
AllImages, AllImages,
List, Stack,
} from "./pages"; } from "./pages";
import { SearchImageContextProvider } from "@contexts/SearchImageContext"; import { SearchImageContextProvider } from "@contexts/SearchImageContext";
import { WithNotifications } from "@contexts/Notifications"; import { WithNotifications } from "@contexts/Notifications";
@ -41,7 +41,7 @@ export const App = () => {
path="/image/:imageId" path="/image/:imageId"
component={ImagePage} component={ImagePage}
/> />
<Route path="/list/:listId" component={List} /> <Route path="/stack/:stackID" component={Stack} />
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
</Route> </Route>
</Route> </Route>

View File

@ -1,4 +1,4 @@
import { List } from "@network/index"; import { Stack } from "@network/index";
import { Component } from "solid-js"; import { Component } from "solid-js";
import fastHashCode from "../../utils/hash"; import fastHashCode from "../../utils/hash";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
@ -17,19 +17,19 @@ const colors = [
"bg-pink-50", "bg-pink-50",
]; ];
export const StackCard: Component<{ list: List }> = (props) => { export const StackCard: Component<{ stack: Stack }> = (props) => {
return ( return (
<A <A
href={`/list/${props.list.ID}`} href={`/stack/${props.stack.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.stack.Name, { forcePositive: true }) % colors.length
] ]
} }
> >
<p class="text-xl font-bold">{props.list.Name}</p> <p class="text-xl font-bold">{props.stack.Name}</p>
<p class="text-lg">{props.list.Images.length}</p> <p class="text-lg">{props.stack.Images.length}</p>
</A> </A>
); );
}; };

View File

@ -133,7 +133,7 @@ export const deleteStackItem = async (
await fetch(request); await fetch(request);
} }
export const deleteList = async (listID: string): Promise<void> => { export const deleteStack = async (listID: string): Promise<void> => {
const request = await getBaseAuthorizedRequest({ const request = await getBaseAuthorizedRequest({
path: `stacks/${listID}`, path: `stacks/${listID}`,
method: "DELETE", method: "DELETE",
@ -238,7 +238,7 @@ const stackValidator = strictObject({
SchemaItems: array(stackSchemaItem), SchemaItems: array(stackSchemaItem),
}); });
export type List = InferOutput<typeof stackValidator>; export type Stack = InferOutput<typeof stackValidator>;
const imageRequestValidator = strictObject({ const imageRequestValidator = strictObject({
UserImages: array(userImageValidator), UserImages: array(userImageValidator),
@ -303,13 +303,13 @@ export const postCode = async (
return parsedRes.output; return parsedRes.output;
}; };
export class ReachedListLimit extends Error { export class ReachedStackLimit extends Error {
constructor() { constructor() {
super(); super();
} }
} }
export const createList = async ( export const createStack = async (
title: string, title: string,
description: string, description: string,
): Promise<void> => { ): Promise<void> => {
@ -323,6 +323,6 @@ export const createList = async (
const res = await fetch(request); const res = await fetch(request);
if (!res.ok && res.status == 429) { if (!res.ok && res.status == 429) {
throw new ReachedListLimit(); throw new ReachedStackLimit();
} }
}; };

View File

@ -1,10 +1,10 @@
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 { 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 { createStack, ReachedStackLimit } from "../../network";
import { createToast } from "../../utils/show-toast"; import { createToast } from "../../utils/show-toast";
import { StackCard } from "@components/stack-card";
export const Categories: Component = () => { export const Categories: Component = () => {
const { stacks, onRefetchImages } = useSearchImageContext(); const { stacks, onRefetchImages } = useSearchImageContext();
@ -15,20 +15,20 @@ export const Categories: Component = () => {
const [isCreating, setIsCreating] = createSignal(false); const [isCreating, setIsCreating] = createSignal(false);
const [showForm, setShowForm] = createSignal(false); const [showForm, setShowForm] = createSignal(false);
const handleCreateList = async () => { const handleCreatestack = async () => {
if (description().trim().length === 0 || title().trim().length === 0) if (description().trim().length === 0 || title().trim().length === 0)
return; return;
setIsCreating(true); setIsCreating(true);
try { try {
await createList(title().trim(), description().trim()); await createStack(title().trim(), description().trim());
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setShowForm(false); setShowForm(false);
onRefetchImages(); // Refresh the stacks onRefetchImages(); // Refresh the stacks
} catch (error) { } catch (error) {
console.error("Failed to create list:", error); console.error("Failed to create stack:", error);
if (error instanceof ReachedListLimit) { if (error instanceof ReachedStackLimit) {
createToast("Reached limit!", "You've reached your limit for new stacks"); createToast("Reached limit!", "You've reached your limit for new stacks");
} }
} finally { } finally {
@ -40,7 +40,7 @@ export const Categories: Component = () => {
<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 stacks</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={stacks()}>{(list) => <StackCard list={list} />}</For> <For each={stacks()}>{(stack) => <StackCard stack={stack} />}</For>
</div> </div>
<div class="mt-4"> <div class="mt-4">
@ -48,7 +48,7 @@ export const Categories: Component = () => {
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium shadow-sm hover:shadow-md" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium shadow-sm hover:shadow-md"
onClick={() => setShowForm(true)} onClick={() => setShowForm(true)}
> >
+ Create List + Create stack
</Button> </Button>
</div> </div>
@ -59,25 +59,25 @@ export const Categories: Component = () => {
<Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto"> <Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div class="p-6"> <div class="p-6">
<Dialog.Title class="text-xl font-bold text-neutral-900 mb-4"> <Dialog.Title class="text-xl font-bold text-neutral-900 mb-4">
Create New List Create New stack
</Dialog.Title> </Dialog.Title>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<label <label
for="list-title" for="stack-title"
class="block text-sm font-medium text-neutral-700 mb-2" class="block text-sm font-medium text-neutral-700 mb-2"
> >
List Title stack Title
</label> </label>
<input <input
id="list-title" id="stack-title"
type="text" type="text"
value={title()} value={title()}
onInput={(e) => onInput={(e) =>
setTitle(e.target.value) setTitle(e.target.value)
} }
placeholder="Enter a title for your list" placeholder="Enter a title for your stack"
class="w-full p-3 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors" class="w-full p-3 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors"
disabled={isCreating()} disabled={isCreating()}
/> />
@ -85,18 +85,18 @@ export const Categories: Component = () => {
<div> <div>
<label <label
for="list-description" for="stack-description"
class="block text-sm font-medium text-neutral-700 mb-2" class="block text-sm font-medium text-neutral-700 mb-2"
> >
List Description stack Description
</label> </label>
<textarea <textarea
id="list-description" id="stack-description"
value={description()} value={description()}
onInput={(e) => onInput={(e) =>
setDescription(e.target.value) setDescription(e.target.value)
} }
placeholder="Describe what kind of list you want to create (e.g., 'A list of my favorite recipes' or 'Photos from my vacation')" placeholder="Describe what kind of stack you want to create (e.g., 'A list of my favorite recipes' or 'Photos from my vacation')"
class="w-full p-3 border border-neutral-300 rounded-lg resize-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors" class="w-full p-3 border border-neutral-300 rounded-lg resize-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors"
rows="4" rows="4"
disabled={isCreating()} disabled={isCreating()}
@ -107,7 +107,7 @@ export const Categories: Component = () => {
<div class="flex gap-3 mt-6"> <div class="flex gap-3 mt-6">
<Button <Button
class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 font-medium shadow-sm hover:shadow-md" class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 font-medium shadow-sm hover:shadow-md"
onClick={handleCreateList} onClick={handleCreatestack}
disabled={ disabled={
isCreating() || isCreating() ||
!title().trim() || !title().trim() ||
@ -116,7 +116,7 @@ export const Categories: Component = () => {
> >
{isCreating() {isCreating()
? "Creating..." ? "Creating..."
: "Create List"} : "Create stack"}
</Button> </Button>
<Button <Button
class="px-4 py-2 bg-neutral-300 text-neutral-700 rounded-lg hover:bg-neutral-400 transition-colors font-medium" class="px-4 py-2 bg-neutral-300 text-neutral-700 rounded-lg hover:bg-neutral-400 transition-colors font-medium"

View File

@ -1,9 +1,9 @@
import { ImageComponentFullHeight } from "@components/image"; import { ImageComponentFullHeight } from "@components/image";
import { StackCard } from "@components/stack-card";
import { useSearchImageContext } from "@contexts/SearchImageContext"; 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 { StackCard } from "@components/list-card";
export const ImagePage: Component = () => { export const ImagePage: Component = () => {
const { imageId } = useParams<{ imageId: string }>(); const { imageId } = useParams<{ imageId: string }>();
@ -27,7 +27,7 @@ export const ImagePage: Component = () => {
<For each={image()?.ImageStacks}> <For each={image()?.ImageStacks}>
{(imageList) => ( {(imageList) => (
<StackCard <StackCard
list={stacks().find((l) => l.ID === imageList.ListID)!} stack={stacks().find((l) => l.ID === imageList.ListID)!}
/> />
)} )}
</For> </For>

View File

@ -4,4 +4,4 @@ export * from "./settings";
export * from "./login"; export * from "./login";
export * from "./search"; export * from "./search";
export * from "./all-images"; export * from "./all-images";
export * from "./list"; export * from "./stack";

View File

@ -8,7 +8,7 @@ import {
createResource, createResource,
createSignal, createSignal,
} from "solid-js"; } from "solid-js";
import { base, deleteList, getAccessToken } from "../../network"; import { base, deleteStack, 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) => {
@ -57,7 +57,7 @@ const DeleteButton: Component<{ onDelete: () => void }> = (props) => {
); );
}; };
const DeleteListButton: Component<{ onDelete: () => void }> = (props) => { const DeleteStackButton: Component<{ onDelete: () => void }> = (props) => {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
return ( return (
@ -67,7 +67,7 @@ const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
class="text-white bg-red-600 hover:bg-red-700 rounded px-3 py-2 text-sm font-medium" class="text-white bg-red-600 hover:bg-red-700 rounded px-3 py-2 text-sm font-medium"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
Delete List Delete Stack
</button> </button>
<Dialog.Root open={isOpen()} onOpenChange={setIsOpen}> <Dialog.Root open={isOpen()} onOpenChange={setIsOpen}>
@ -76,7 +76,7 @@ const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
<div class="fixed inset-0 z-50 flex items-center justify-center p-4"> <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.Content class="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<Dialog.Title class="text-lg font-bold mb-2"> <Dialog.Title class="text-lg font-bold mb-2">
Confirm Delete List Confirm Delete Stack
</Dialog.Title> </Dialog.Title>
<Dialog.Description class="mb-4"> <Dialog.Description class="mb-4">
Are you sure you want to delete this entire Are you sure you want to delete this entire
@ -92,7 +92,7 @@ const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
onClick={props.onDelete} onClick={props.onDelete}
> >
Delete List Delete Stack
</button> </button>
</div> </div>
</Dialog.Content> </Dialog.Content>
@ -103,39 +103,39 @@ const DeleteListButton: Component<{ onDelete: () => void }> = (props) => {
); );
}; };
export const List: Component = () => { export const Stack: Component = () => {
const { listId } = useParams(); const { stackID } = useParams();
const nav = useNavigate(); const nav = useNavigate();
const { stacks, onDeleteImageFromStack } = useSearchImageContext(); const { stacks, onDeleteImageFromStack } = useSearchImageContext();
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
const list = () => stacks().find((l) => l.ID === listId); const stack = () => stacks().find((l) => l.ID === stackID);
const handleDeleteList = async () => { const handleDeleteStack = async () => {
await deleteList(listId) await deleteStack(stackID)
nav("/"); nav("/");
}; };
return ( return (
<Suspense> <Suspense>
<Show when={list()} fallback="List could not be found"> <Show when={stack()} fallback="Stack could not be found">
{(l) => ( {(s) => (
<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="px-6 py-4 border-b border-neutral-200 bg-neutral-50"> <div class="px-6 py-4 border-b border-neutral-200 bg-neutral-50">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-xl font-semibold text-neutral-900"> <h1 class="text-xl font-semibold text-neutral-900">
{l().Name} {s().Name}
</h1> </h1>
<Show when={l().Description}> <Show when={s().Description}>
<p class="text-sm text-neutral-600 mt-1"> <p class="text-sm text-neutral-600 mt-1">
{l().Description} {s().Description}
</p> </p>
</Show> </Show>
</div> </div>
<DeleteListButton onDelete={handleDeleteList} /> <DeleteStackButton onDelete={handleDeleteStack} />
</div> </div>
</div> </div>
<div <div
@ -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().SchemaItems}> <For each={s().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().SchemaItems s().SchemaItems
.length - .length -
1 1
? "border-r border-neutral-200" ? "border-r border-neutral-200"
@ -166,7 +166,7 @@ export const List: Component = () => {
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-neutral-200"> <tbody class="divide-y divide-neutral-200">
<For each={l().Images}> <For each={s().Images}>
{(image, rowIndex) => ( {(image, rowIndex) => (
<tr <tr
class={`hover:bg-neutral-50 transition-colors ${rowIndex() % 2 === 0 class={`hover:bg-neutral-50 transition-colors ${rowIndex() % 2 === 0
@ -183,13 +183,13 @@ export const List: Component = () => {
<img <img
class="w-full h-full object-cover rounded-lg" class="w-full h-full object-cover rounded-lg"
src={`${base}/images/${image.ImageID}?token=${accessToken()}`} src={`${base}/images/${image.ImageID}?token=${accessToken()}`}
alt="List item" alt="Stack item"
/> />
</a> </a>
<DeleteButton <DeleteButton
onDelete={() => onDelete={() =>
onDeleteImageFromStack( onDeleteImageFromStack(
l().ID, s().ID,
image.ImageID, image.ImageID,
) )
} }
@ -223,7 +223,7 @@ export const List: Component = () => {
</For> </For>
</tbody> </tbody>
</table> </table>
<Show when={l().Images.length === 0}> <Show when={s().Images.length === 0}>
<div class="px-6 py-12 text-center text-neutral-500"> <div class="px-6 py-12 text-center text-neutral-500">
<p class="text-lg"> <p class="text-lg">
No images in this list yet No images in this list yet