am i finished?
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
@ -11,7 +11,9 @@ type MailClient struct {
|
||||
client *mail.Client
|
||||
}
|
||||
|
||||
type TestMailClient struct{}
|
||||
type TestMailClient struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Mailer interface {
|
||||
SendCode(to string, code string) error
|
||||
@ -43,15 +45,17 @@ func (m MailClient) SendCode(to string, code string) error {
|
||||
}
|
||||
|
||||
func (m TestMailClient) SendCode(to string, code string) error {
|
||||
fmt.Printf("Email: %s | Code %s\n", to, code)
|
||||
m.logger.Info("Auth Code", "email", to, "code", code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateMailClient() (Mailer, error) {
|
||||
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||
mode := os.Getenv("MODE")
|
||||
if mode == "DEV" {
|
||||
return TestMailClient{}, nil
|
||||
return TestMailClient{
|
||||
log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := mail.NewClient(
|
||||
|
@ -35,7 +35,6 @@ type codeReturn struct {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: validate email
|
||||
err := h.auth.CreateCode(body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
||||
@ -92,7 +91,7 @@ func CreateAuthHandler(db *sql.DB) AuthHandler {
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Auth")
|
||||
|
||||
mailer, err := CreateMailClient()
|
||||
mailer, err := CreateMailClient(logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -145,15 +145,15 @@ func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting image router")
|
||||
|
||||
// Public route for serving images (not protected)
|
||||
r.Get("/image/{id}", h.serveImage)
|
||||
r.Get("/{id}", h.serveImage)
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute)
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/image", h.listImages)
|
||||
r.Post("/image/{name}", h.uploadImage)
|
||||
r.Get("/", h.listImages)
|
||||
r.Post("/{name}", h.uploadImage)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -358,7 +358,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
|
||||
t.Run("Image Routes", func(t *testing.T) {
|
||||
t.Run("Get images without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", "", nil)
|
||||
resp := tc.makeRequest(t, "GET", "/images/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
@ -367,7 +367,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Get images with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", imageUser.Token, nil)
|
||||
resp := tc.makeRequest(t, "GET", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@ -384,7 +384,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
// Create a simple valid base64 string for testing
|
||||
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/image/test.png", strings.NewReader(testImageBase64))
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test.png", strings.NewReader(testImageBase64))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
@ -417,7 +417,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/image/test2.png", bytes.NewReader(testImageBinary))
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test2.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
@ -440,7 +440,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Upload image without name", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "POST", "/images/image/", imageUser.Token, nil)
|
||||
resp := tc.makeRequest(t, "POST", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Route pattern doesn't match empty names, so returns 404
|
||||
@ -451,7 +451,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
|
||||
t.Run("Serve non-existent image", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "GET", "/images/image/"+fakeUUID.String(), "", nil)
|
||||
resp := tc.makeRequest(t, "GET", "/images/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
@ -462,7 +462,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
|
||||
t.Run("Complete User Flow", func(t *testing.T) {
|
||||
// Step 1: Test authentication is working
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", flowUser.Token, nil)
|
||||
resp := tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
@ -478,7 +478,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/image/test_flow.png", bytes.NewReader(testImageBinary))
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test_flow.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create upload request: %v", err)
|
||||
}
|
||||
@ -500,7 +500,7 @@ func TestAllRoutes(t *testing.T) {
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 3: Verify image appears in user's image list
|
||||
resp = tc.makeRequest(t, "GET", "/images/image", flowUser.Token, nil)
|
||||
resp = tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export const ImageComponent: Component<{ ID: string }> = (props) => {
|
||||
<A href={`/image/${props.ID}`} class="w-full flex justify-center h-[300px]">
|
||||
<img
|
||||
class="flex w-full object-cover rounded-xl"
|
||||
src={`${base}/image/${props.ID}`}
|
||||
src={`${base}/images/${props.ID}`}
|
||||
/>
|
||||
</A>
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ export const ProcessingImages: Component = () => {
|
||||
<img
|
||||
class="w-16 h-16 aspect-square rounded"
|
||||
alt="processing"
|
||||
src={`${base}/image/${id}`}
|
||||
src={`${base}/images/${id}`}
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-slate-100">{image().ImageName}</p>
|
||||
|
@ -14,12 +14,12 @@ export type SearchImageStore = {
|
||||
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
|
||||
>;
|
||||
|
||||
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["Lists"]>;
|
||||
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["lists"]>;
|
||||
|
||||
userImages: Accessor<JustTheImageWhatAreTheseNames>;
|
||||
|
||||
processingImages: Accessor<
|
||||
Awaited<ReturnType<typeof getUserImages>>["ProcessingImages"] | undefined
|
||||
Awaited<ReturnType<typeof getUserImages>>["processingImages"] | undefined
|
||||
>;
|
||||
|
||||
onRefetchImages: () => void;
|
||||
@ -39,7 +39,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;
|
||||
}
|
||||
@ -58,14 +58,14 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const processingImages = () => data()?.ProcessingImages ?? [];
|
||||
const processingImages = () => data()?.processingImages ?? [];
|
||||
|
||||
return (
|
||||
<SearchImageContext.Provider
|
||||
value={{
|
||||
imagesByDate: sortedImages,
|
||||
lists: () => data()?.Lists ?? [],
|
||||
userImages: () => data()?.UserImages ?? [],
|
||||
lists: () => data()?.lists ?? [],
|
||||
userImages: () => data()?.userImages ?? [],
|
||||
processingImages,
|
||||
onRefetchImages: refetch,
|
||||
}}
|
||||
|
@ -55,7 +55,7 @@ export const sendImageFile = async (
|
||||
file: File,
|
||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: `image/${imageName}`,
|
||||
path: `images/${imageName}`,
|
||||
body: file,
|
||||
method: "POST",
|
||||
});
|
||||
@ -72,7 +72,7 @@ export const sendImage = async (
|
||||
base64Image: string,
|
||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: `image/${imageName}`,
|
||||
path: `images/${imageName}`,
|
||||
body: base64Image,
|
||||
method: "POST",
|
||||
});
|
||||
@ -161,9 +161,9 @@ const listValidator = strictObject({
|
||||
export type List = InferOutput<typeof listValidator>;
|
||||
|
||||
const imageRequestValidator = strictObject({
|
||||
UserImages: array(userImageValidator),
|
||||
ProcessingImages: array(userProcessingImageValidator),
|
||||
Lists: array(listValidator),
|
||||
userImages: array(userImageValidator),
|
||||
processingImages: array(userProcessingImageValidator),
|
||||
lists: array(listValidator),
|
||||
});
|
||||
|
||||
export type JustTheImageWhatAreTheseNames = InferOutput<
|
||||
@ -173,18 +173,16 @@ export type JustTheImageWhatAreTheseNames = InferOutput<
|
||||
export const getUserImages = async (): Promise<
|
||||
InferOutput<typeof imageRequestValidator>
|
||||
> => {
|
||||
const request = getBaseAuthorizedRequest({ path: "image" });
|
||||
const request = getBaseAuthorizedRequest({ path: "images" });
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
|
||||
console.log("BACKEND RESPONSE: ", res);
|
||||
|
||||
return parse(imageRequestValidator, res);
|
||||
};
|
||||
|
||||
export const postLogin = async (email: string): Promise<void> => {
|
||||
const request = getBaseRequest({
|
||||
path: "login",
|
||||
path: "auth/login",
|
||||
body: JSON.stringify({ email }),
|
||||
method: "POST",
|
||||
});
|
||||
@ -192,18 +190,6 @@ export const postLogin = async (email: string): Promise<void> => {
|
||||
await fetch(request);
|
||||
};
|
||||
|
||||
export const postDemoLogin = async (): Promise<
|
||||
InferOutput<typeof codeValidator>
|
||||
> => {
|
||||
const request = getBaseRequest({
|
||||
path: "demo-login",
|
||||
});
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
|
||||
return parse(codeValidator, res);
|
||||
};
|
||||
|
||||
const codeValidator = strictObject({
|
||||
access: string(),
|
||||
refresh: string(),
|
||||
@ -214,7 +200,7 @@ export const postCode = async (
|
||||
code: string,
|
||||
): Promise<InferOutput<typeof codeValidator>> => {
|
||||
const request = getBaseRequest({
|
||||
path: "code",
|
||||
path: "auth/code",
|
||||
body: JSON.stringify({ email, code }),
|
||||
method: "POST",
|
||||
});
|
||||
@ -223,3 +209,18 @@ export const postCode = async (
|
||||
|
||||
return parse(codeValidator, res);
|
||||
};
|
||||
|
||||
export const createList = async (
|
||||
title: string,
|
||||
description: string,
|
||||
): Promise<void> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: "stacks",
|
||||
method: "POST",
|
||||
body: JSON.stringify({ title, description }),
|
||||
});
|
||||
|
||||
request.headers.set("Content-Type", "application/json");
|
||||
|
||||
await fetch(request);
|
||||
};
|
||||
|
@ -1,16 +1,136 @@
|
||||
import { Component, For } from "solid-js";
|
||||
import { Component, For, createSignal } from "solid-js";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { ListCard } from "@components/list-card";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { Dialog } from "@kobalte/core/dialog";
|
||||
import { createList } from "../../network";
|
||||
|
||||
export const Categories: Component = () => {
|
||||
const { lists } = useSearchImageContext();
|
||||
const { lists, onRefetchImages } = useSearchImageContext();
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
|
||||
<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()}>{(list) => <ListCard list={list} />}</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const [title, setTitle] = createSignal("");
|
||||
const [description, setDescription] = createSignal("");
|
||||
|
||||
const [isCreating, setIsCreating] = createSignal(false);
|
||||
const [showForm, setShowForm] = createSignal(false);
|
||||
|
||||
const handleCreateList = async () => {
|
||||
if (description().trim().length === 0 || title().trim().length === 0)
|
||||
return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await createList(title().trim(), description().trim());
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setShowForm(false);
|
||||
onRefetchImages(); // Refresh the lists
|
||||
} catch (error) {
|
||||
console.error("Failed to create list:", error);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
|
||||
<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()}>{(list) => <ListCard list={list} />}</For>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
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)}
|
||||
>
|
||||
+ Create List
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={showForm()} onOpenChange={setShowForm}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-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 max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<Dialog.Title class="text-xl font-bold text-neutral-900 mb-4">
|
||||
Create New List
|
||||
</Dialog.Title>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="list-title"
|
||||
class="block text-sm font-medium text-neutral-700 mb-2"
|
||||
>
|
||||
List Title
|
||||
</label>
|
||||
<input
|
||||
id="list-title"
|
||||
type="text"
|
||||
value={title()}
|
||||
onInput={(e) =>
|
||||
setTitle(e.target.value)
|
||||
}
|
||||
placeholder="Enter a title for your list"
|
||||
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()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="list-description"
|
||||
class="block text-sm font-medium text-neutral-700 mb-2"
|
||||
>
|
||||
List Description
|
||||
</label>
|
||||
<textarea
|
||||
id="list-description"
|
||||
value={description()}
|
||||
onInput={(e) =>
|
||||
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')"
|
||||
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"
|
||||
disabled={isCreating()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<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"
|
||||
onClick={handleCreateList}
|
||||
disabled={
|
||||
isCreating() ||
|
||||
!title().trim() ||
|
||||
!description().trim()
|
||||
}
|
||||
>
|
||||
{isCreating()
|
||||
? "Creating..."
|
||||
: "Create List"}
|
||||
</Button>
|
||||
<Button
|
||||
class="px-4 py-2 bg-neutral-300 text-neutral-700 rounded-lg hover:bg-neutral-400 transition-colors font-medium"
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
}}
|
||||
disabled={isCreating()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -3,7 +3,6 @@ import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { For, type Component } from "solid-js";
|
||||
import SolidjsMarkdown from "solidjs-markdown";
|
||||
import { List } from "../list";
|
||||
import { ListCard } from "@components/list-card";
|
||||
|
||||
export const ImagePage: Component = () => {
|
||||
|
@ -1,43 +1,107 @@
|
||||
import { ImageComponent } from "@components/image";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { base } from "../../network";
|
||||
|
||||
export const List: Component = () => {
|
||||
const { listId } = useParams();
|
||||
const { listId } = useParams();
|
||||
|
||||
const { lists } = useSearchImageContext();
|
||||
const { lists } = useSearchImageContext();
|
||||
|
||||
const list = () => lists().find((l) => l.ID === listId);
|
||||
const list = () => lists().find((l) => l.ID === listId);
|
||||
|
||||
return (
|
||||
<Show when={list()} fallback="List could not be found">
|
||||
{(l) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<For each={l().Schema.SchemaItems}>
|
||||
{(item) => <th>{item.Item}</th>}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={l().Images}>
|
||||
{(image) => (
|
||||
<tr>
|
||||
<td>
|
||||
<ImageComponent ID={image.ImageID} />
|
||||
</td>
|
||||
<For each={image.Items}>
|
||||
{(item) => <td>{item.Value}</td>}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
return (
|
||||
<Show when={list()} fallback="List could not be found">
|
||||
{(l) => (
|
||||
<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">
|
||||
<table class="w-full min-w-full">
|
||||
<thead class="bg-neutral-50 border-b border-neutral-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<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}>
|
||||
{(item, index) => (
|
||||
<th
|
||||
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${
|
||||
index() <
|
||||
l().Schema.SchemaItems
|
||||
.length -
|
||||
1
|
||||
? "border-r border-neutral-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{item.Item}
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-200">
|
||||
<For each={l().Images}>
|
||||
{(image, rowIndex) => (
|
||||
<tr
|
||||
class={`hover:bg-neutral-50 transition-colors ${
|
||||
rowIndex() % 2 === 0
|
||||
? "bg-white"
|
||||
: "bg-neutral-25"
|
||||
}`}
|
||||
>
|
||||
<td class="px-6 py-4 border-r border-neutral-200">
|
||||
<div class="w-32 h-24 overflow-hidden rounded-lg">
|
||||
<a
|
||||
href={`/image/${image.ImageID}`}
|
||||
class="w-full h-full flex justify-center"
|
||||
>
|
||||
<img
|
||||
class="w-full h-full object-cover rounded-lg"
|
||||
src={`${base}/images/${image.ImageID}`}
|
||||
alt="List item"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<For each={image.Items}>
|
||||
{(item, colIndex) => (
|
||||
<td
|
||||
class={`px-6 py-4 text-sm text-neutral-700 ${
|
||||
colIndex() <
|
||||
image.Items.length -
|
||||
1
|
||||
? "border-r border-neutral-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
class="max-w-xs truncate"
|
||||
title={item.Value}
|
||||
>
|
||||
{item.Value}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
<Show when={l().Images.length === 0}>
|
||||
<div class="px-6 py-12 text-center text-neutral-500">
|
||||
<p class="text-lg">
|
||||
No images in this list yet
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
Images will appear here once added to the
|
||||
list
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { isTokenValid } from "@components/protected-route";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { TextField } from "@kobalte/core/text-field";
|
||||
import { postCode, postDemoLogin, postLogin } from "@network/index";
|
||||
import { postCode, postLogin } from "@network/index";
|
||||
import { Navigate } from "@solidjs/router";
|
||||
import { type Component, Show, createSignal } from "solid-js";
|
||||
|
||||
@ -18,16 +18,6 @@ export const Login: Component = () => {
|
||||
throw new Error("bruh, no email");
|
||||
}
|
||||
|
||||
if (email.toString() === "demo@email.com") {
|
||||
const { access, refresh } = await postDemoLogin();
|
||||
|
||||
localStorage.setItem("access", access);
|
||||
localStorage.setItem("refresh", refresh);
|
||||
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!submitted()) {
|
||||
await postLogin(email.toString());
|
||||
setSubmitted(true);
|
||||
|
Reference in New Issue
Block a user