am i finished?
This commit is contained in:
@ -1,9 +1,9 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"github.com/wneessen/go-mail"
|
"github.com/wneessen/go-mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -11,7 +11,9 @@ type MailClient struct {
|
|||||||
client *mail.Client
|
client *mail.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestMailClient struct{}
|
type TestMailClient struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
type Mailer interface {
|
type Mailer interface {
|
||||||
SendCode(to string, code string) error
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateMailClient() (Mailer, error) {
|
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||||
mode := os.Getenv("MODE")
|
mode := os.Getenv("MODE")
|
||||||
if mode == "DEV" {
|
if mode == "DEV" {
|
||||||
return TestMailClient{}, nil
|
return TestMailClient{
|
||||||
|
log,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := mail.NewClient(
|
client, err := mail.NewClient(
|
||||||
|
@ -35,7 +35,6 @@ type codeReturn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: validate email
|
|
||||||
err := h.auth.CreateCode(body.Email)
|
err := h.auth.CreateCode(body.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
||||||
@ -92,7 +91,7 @@ func CreateAuthHandler(db *sql.DB) AuthHandler {
|
|||||||
userModel := models.NewUserModel(db)
|
userModel := models.NewUserModel(db)
|
||||||
logger := log.New(os.Stdout).WithPrefix("Auth")
|
logger := log.New(os.Stdout).WithPrefix("Auth")
|
||||||
|
|
||||||
mailer, err := CreateMailClient()
|
mailer, err := CreateMailClient(logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -145,15 +145,15 @@ func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
|||||||
h.logger.Info("Mounting image router")
|
h.logger.Info("Mounting image router")
|
||||||
|
|
||||||
// Public route for serving images (not protected)
|
// Public route for serving images (not protected)
|
||||||
r.Get("/image/{id}", h.serveImage)
|
r.Get("/{id}", h.serveImage)
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(middleware.ProtectedRoute)
|
r.Use(middleware.ProtectedRoute)
|
||||||
r.Use(middleware.SetJson)
|
r.Use(middleware.SetJson)
|
||||||
|
|
||||||
r.Get("/image", h.listImages)
|
r.Get("/", h.listImages)
|
||||||
r.Post("/image/{name}", h.uploadImage)
|
r.Post("/{name}", h.uploadImage)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,7 +358,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Image Routes", func(t *testing.T) {
|
t.Run("Image Routes", func(t *testing.T) {
|
||||||
t.Run("Get images without authentication", 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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusUnauthorized {
|
if resp.StatusCode != http.StatusUnauthorized {
|
||||||
@ -367,7 +367,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Get images with authentication", func(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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
@ -384,7 +384,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
// Create a simple valid base64 string for testing
|
// Create a simple valid base64 string for testing
|
||||||
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create request: %v", err)
|
t.Fatalf("Failed to create request: %v", err)
|
||||||
}
|
}
|
||||||
@ -417,7 +417,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create request: %v", err)
|
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) {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Route pattern doesn't match empty names, so returns 404
|
// 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) {
|
t.Run("Serve non-existent image", func(t *testing.T) {
|
||||||
fakeUUID := uuid.New()
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNotFound {
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
@ -462,7 +462,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Complete User Flow", func(t *testing.T) {
|
t.Run("Complete User Flow", func(t *testing.T) {
|
||||||
// Step 1: Test authentication is working
|
// 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 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
|
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,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create upload request: %v", err)
|
t.Fatalf("Failed to create upload request: %v", err)
|
||||||
}
|
}
|
||||||
@ -500,7 +500,7 @@ func TestAllRoutes(t *testing.T) {
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
// Step 3: Verify image appears in user's image list
|
// 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 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
|
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]">
|
<A href={`/image/${props.ID}`} class="w-full flex justify-center h-[300px]">
|
||||||
<img
|
<img
|
||||||
class="flex w-full object-cover rounded-xl"
|
class="flex w-full object-cover rounded-xl"
|
||||||
src={`${base}/image/${props.ID}`}
|
src={`${base}/images/${props.ID}`}
|
||||||
/>
|
/>
|
||||||
</A>
|
</A>
|
||||||
);
|
);
|
||||||
|
@ -43,7 +43,7 @@ export const ProcessingImages: Component = () => {
|
|||||||
<img
|
<img
|
||||||
class="w-16 h-16 aspect-square rounded"
|
class="w-16 h-16 aspect-square rounded"
|
||||||
alt="processing"
|
alt="processing"
|
||||||
src={`${base}/image/${id}`}
|
src={`${base}/images/${id}`}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="text-slate-100">{image().ImageName}</p>
|
<p class="text-slate-100">{image().ImageName}</p>
|
||||||
|
@ -14,12 +14,12 @@ export type SearchImageStore = {
|
|||||||
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
|
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["Lists"]>;
|
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["lists"]>;
|
||||||
|
|
||||||
userImages: Accessor<JustTheImageWhatAreTheseNames>;
|
userImages: Accessor<JustTheImageWhatAreTheseNames>;
|
||||||
|
|
||||||
processingImages: Accessor<
|
processingImages: Accessor<
|
||||||
Awaited<ReturnType<typeof getUserImages>>["ProcessingImages"] | undefined
|
Awaited<ReturnType<typeof getUserImages>>["processingImages"] | undefined
|
||||||
>;
|
>;
|
||||||
|
|
||||||
onRefetchImages: () => void;
|
onRefetchImages: () => void;
|
||||||
@ -39,7 +39,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;
|
||||||
}
|
}
|
||||||
@ -58,14 +58,14 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const processingImages = () => data()?.ProcessingImages ?? [];
|
const processingImages = () => data()?.processingImages ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchImageContext.Provider
|
<SearchImageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
imagesByDate: sortedImages,
|
imagesByDate: sortedImages,
|
||||||
lists: () => data()?.Lists ?? [],
|
lists: () => data()?.lists ?? [],
|
||||||
userImages: () => data()?.UserImages ?? [],
|
userImages: () => data()?.userImages ?? [],
|
||||||
processingImages,
|
processingImages,
|
||||||
onRefetchImages: refetch,
|
onRefetchImages: refetch,
|
||||||
}}
|
}}
|
||||||
|
@ -55,7 +55,7 @@ export const sendImageFile = async (
|
|||||||
file: File,
|
file: File,
|
||||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||||
const request = getBaseAuthorizedRequest({
|
const request = getBaseAuthorizedRequest({
|
||||||
path: `image/${imageName}`,
|
path: `images/${imageName}`,
|
||||||
body: file,
|
body: file,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@ -72,7 +72,7 @@ export const sendImage = async (
|
|||||||
base64Image: string,
|
base64Image: string,
|
||||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||||
const request = getBaseAuthorizedRequest({
|
const request = getBaseAuthorizedRequest({
|
||||||
path: `image/${imageName}`,
|
path: `images/${imageName}`,
|
||||||
body: base64Image,
|
body: base64Image,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@ -161,9 +161,9 @@ const listValidator = strictObject({
|
|||||||
export type List = InferOutput<typeof listValidator>;
|
export type List = InferOutput<typeof listValidator>;
|
||||||
|
|
||||||
const imageRequestValidator = strictObject({
|
const imageRequestValidator = strictObject({
|
||||||
UserImages: array(userImageValidator),
|
userImages: array(userImageValidator),
|
||||||
ProcessingImages: array(userProcessingImageValidator),
|
processingImages: array(userProcessingImageValidator),
|
||||||
Lists: array(listValidator),
|
lists: array(listValidator),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type JustTheImageWhatAreTheseNames = InferOutput<
|
export type JustTheImageWhatAreTheseNames = InferOutput<
|
||||||
@ -173,18 +173,16 @@ export type JustTheImageWhatAreTheseNames = InferOutput<
|
|||||||
export const getUserImages = async (): Promise<
|
export const getUserImages = async (): Promise<
|
||||||
InferOutput<typeof imageRequestValidator>
|
InferOutput<typeof imageRequestValidator>
|
||||||
> => {
|
> => {
|
||||||
const request = getBaseAuthorizedRequest({ path: "image" });
|
const request = getBaseAuthorizedRequest({ path: "images" });
|
||||||
|
|
||||||
const res = await fetch(request).then((res) => res.json());
|
const res = await fetch(request).then((res) => res.json());
|
||||||
|
|
||||||
console.log("BACKEND RESPONSE: ", res);
|
|
||||||
|
|
||||||
return parse(imageRequestValidator, res);
|
return parse(imageRequestValidator, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postLogin = async (email: string): Promise<void> => {
|
export const postLogin = async (email: string): Promise<void> => {
|
||||||
const request = getBaseRequest({
|
const request = getBaseRequest({
|
||||||
path: "login",
|
path: "auth/login",
|
||||||
body: JSON.stringify({ email }),
|
body: JSON.stringify({ email }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@ -192,18 +190,6 @@ export const postLogin = async (email: string): Promise<void> => {
|
|||||||
await fetch(request);
|
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({
|
const codeValidator = strictObject({
|
||||||
access: string(),
|
access: string(),
|
||||||
refresh: string(),
|
refresh: string(),
|
||||||
@ -214,7 +200,7 @@ export const postCode = async (
|
|||||||
code: string,
|
code: string,
|
||||||
): Promise<InferOutput<typeof codeValidator>> => {
|
): Promise<InferOutput<typeof codeValidator>> => {
|
||||||
const request = getBaseRequest({
|
const request = getBaseRequest({
|
||||||
path: "code",
|
path: "auth/code",
|
||||||
body: JSON.stringify({ email, code }),
|
body: JSON.stringify({ email, code }),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
@ -223,3 +209,18 @@ export const postCode = async (
|
|||||||
|
|
||||||
return parse(codeValidator, res);
|
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 { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||||
import { ListCard } from "@components/list-card";
|
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 = () => {
|
export const Categories: Component = () => {
|
||||||
const { lists } = useSearchImageContext();
|
const { lists, onRefetchImages } = useSearchImageContext();
|
||||||
|
|
||||||
return (
|
const [title, setTitle] = createSignal("");
|
||||||
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
|
const [description, setDescription] = createSignal("");
|
||||||
<h2 class="text-xl font-bold">Generated Lists</h2>
|
|
||||||
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
|
const [isCreating, setIsCreating] = createSignal(false);
|
||||||
<For each={lists()}>{(list) => <ListCard list={list} />}</For>
|
const [showForm, setShowForm] = createSignal(false);
|
||||||
</div>
|
|
||||||
</div>
|
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 { 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 { List } from "../list";
|
|
||||||
import { ListCard } from "@components/list-card";
|
import { ListCard } from "@components/list-card";
|
||||||
|
|
||||||
export const ImagePage: Component = () => {
|
export const ImagePage: Component = () => {
|
||||||
|
@ -1,43 +1,107 @@
|
|||||||
import { ImageComponent } from "@components/image";
|
|
||||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { Component, For, Show } from "solid-js";
|
import { Component, For, Show } from "solid-js";
|
||||||
|
import { base } from "../../network";
|
||||||
|
|
||||||
export const List: Component = () => {
|
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 (
|
return (
|
||||||
<Show when={list()} fallback="List could not be found">
|
<Show when={list()} fallback="List could not be found">
|
||||||
{(l) => (
|
{(l) => (
|
||||||
<table>
|
<div class="w-full h-full bg-white rounded-lg shadow-sm border border-neutral-200 overflow-hidden">
|
||||||
<thead>
|
<div class="overflow-x-auto overflow-y-auto h-full">
|
||||||
<tr>
|
<table class="w-full min-w-full">
|
||||||
<th>Image</th>
|
<thead class="bg-neutral-50 border-b border-neutral-200 sticky top-0 z-10">
|
||||||
<For each={l().Schema.SchemaItems}>
|
<tr>
|
||||||
{(item) => <th>{item.Item}</th>}
|
<th class="px-6 py-4 text-left text-sm font-semibold text-neutral-900 border-r border-neutral-200 min-w-40">
|
||||||
</For>
|
Image
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
<For each={l().Schema.SchemaItems}>
|
||||||
<tbody>
|
{(item, index) => (
|
||||||
<For each={l().Images}>
|
<th
|
||||||
{(image) => (
|
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${
|
||||||
<tr>
|
index() <
|
||||||
<td>
|
l().Schema.SchemaItems
|
||||||
<ImageComponent ID={image.ImageID} />
|
.length -
|
||||||
</td>
|
1
|
||||||
<For each={image.Items}>
|
? "border-r border-neutral-200"
|
||||||
{(item) => <td>{item.Value}</td>}
|
: ""
|
||||||
</For>
|
}`}
|
||||||
</tr>
|
>
|
||||||
)}
|
{item.Item}
|
||||||
</For>
|
</th>
|
||||||
</tbody>
|
)}
|
||||||
</table>
|
</For>
|
||||||
)}
|
</tr>
|
||||||
</Show>
|
</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 { isTokenValid } from "@components/protected-route";
|
||||||
import { Button } from "@kobalte/core/button";
|
import { Button } from "@kobalte/core/button";
|
||||||
import { TextField } from "@kobalte/core/text-field";
|
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 { Navigate } from "@solidjs/router";
|
||||||
import { type Component, Show, createSignal } from "solid-js";
|
import { type Component, Show, createSignal } from "solid-js";
|
||||||
|
|
||||||
@ -18,16 +18,6 @@ export const Login: Component = () => {
|
|||||||
throw new Error("bruh, no email");
|
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()) {
|
if (!submitted()) {
|
||||||
await postLogin(email.toString());
|
await postLogin(email.toString());
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
|
Reference in New Issue
Block a user