diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index 98475b5..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -pnpm test diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100644 index 678f1d9..0000000 --- a/.husky/pre-push +++ /dev/null @@ -1,2 +0,0 @@ -# Full checks before pushing -pnpm lint diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ef9e65e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["carb", "CATEG", "headlessui", "tiktok"] +} diff --git a/src/app/about.txt b/public/about.txt similarity index 100% rename from src/app/about.txt rename to public/about.txt diff --git a/src/app/favicon.ico b/public/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to public/favicon.ico diff --git a/src/app/android-chrome-192x192.png b/public/favicon/android-chrome-192x192.png similarity index 100% rename from src/app/android-chrome-192x192.png rename to public/favicon/android-chrome-192x192.png diff --git a/src/app/android-chrome-512x512.png b/public/favicon/android-chrome-512x512.png similarity index 100% rename from src/app/android-chrome-512x512.png rename to public/favicon/android-chrome-512x512.png diff --git a/src/app/apple-touch-icon.png b/public/favicon/apple-touch-icon.png similarity index 100% rename from src/app/apple-touch-icon.png rename to public/favicon/apple-touch-icon.png diff --git a/src/app/favicon-16x16.png b/public/favicon/favicon-16x16.png similarity index 100% rename from src/app/favicon-16x16.png rename to public/favicon/favicon-16x16.png diff --git a/src/app/favicon-32x32.png b/public/favicon/favicon-32x32.png similarity index 100% rename from src/app/favicon-32x32.png rename to public/favicon/favicon-32x32.png diff --git a/src/api/index.ts b/src/api/index.ts index 12684b3..3e1be8b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -9,6 +9,8 @@ type ApiConfig = RequestInit & { timeout?: number; }; +type ExtendedRequestInit = RequestInit & { includeHeaders?: boolean }; + export class Api { private baseUrl: string; private defaultOptions: RequestInit; @@ -55,7 +57,7 @@ export class Api { method: string, endpoint: string, response: Response, - duration: number, + duration: number ) { if (!this.debugMode) return; @@ -65,7 +67,7 @@ export class Api { headers: Object.fromEntries(response.headers.entries()), statusText: response.statusText, body: await response.clone().json(), - }, + } ); } @@ -73,7 +75,7 @@ export class Api { method: string, endpoint: string, error: Error, - duration: number, + duration: number ) { if (!this.debugMode) return; @@ -88,39 +90,39 @@ export class Api { endpoint: string, options: RequestInit, schema: SchemaType, - includeHeaders: true, + includeHeaders: true ): Promise<{ data: ZodInfer; headers: Headers }>; async request( endpoint: string, options: RequestInit, schema: SchemaType, - includeHeaders?: false, + includeHeaders?: false ): Promise>; async request( endpoint: string, options: RequestInit, - includeHeaders: true, + includeHeaders: true ): Promise<{ data: unknown; headers: Headers }>; async request( endpoint: string, options: RequestInit, - includeHeaders: true, + includeHeaders: true ): Promise<{ data: DataType; headers: Headers }>; async request( endpoint: string, options?: RequestInit, - includeHeaders?: boolean, + includeHeaders?: boolean ): Promise; async request( endpoint: string, options: RequestInit = {}, schemaOrIncludeHeaders?: SchemaType | boolean, - includeHeadersFlag?: boolean, + includeHeadersFlag?: boolean ): Promise { const url = new URL(endpoint, this.baseUrl); const controller = new AbortController(); @@ -170,7 +172,7 @@ export class Api { throw new ApiError( `Non-JSON response from ${url.href}`, 500, - "Invalid Content-Type", + "Invalid Content-Type" ); } @@ -183,7 +185,7 @@ export class Api { if (!parseResult.success) { throw new ValidationError( `Response validation failed for ${url.href}`, - parseResult.error.issues, + parseResult.error.issues ); } parsedData = parseResult.data; @@ -212,7 +214,7 @@ export class Api { "Unknown error occurred", 0, "UNKNOWN_ERROR", - String(error), + String(error) ); } finally { controller.abort(); @@ -223,25 +225,25 @@ export class Api { async get( endpoint: string, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async get( endpoint: string, schema: ZodSchema, - options?: RequestInit, + options?: RequestInit ): Promise; async get( endpoint: string, schema: ZodSchema, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async get( endpoint: string, arg1?: ZodSchema | RequestInit, - arg2?: RequestInit, + arg2?: RequestInit ): Promise { let options: RequestInit = {}; let includeHeaders = false; @@ -264,59 +266,57 @@ export class Api { async post( endpoint: string, body: BodyInit | Record, - options?: RequestInit, + options?: RequestInit ): Promise; async post( endpoint: string, body: BodyInit | Record, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async post( endpoint: string, body: BodyInit | Record, schema: ZodSchema, - options?: RequestInit, + options?: RequestInit ): Promise; async post( endpoint: string, body: BodyInit | Record, schema: ZodSchema, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async post( endpoint: string, - body: BodyInit | Record, - arg1?: ZodSchema | RequestInit, - arg2?: RequestInit, + body: BodyInit | Record | FormData, + arg1?: ZodSchema | ExtendedRequestInit, + arg2?: ExtendedRequestInit ): Promise { let schema: ZodSchema | undefined; - let options: RequestInit = {}; + let options: ExtendedRequestInit = {}; let includeHeaders = false; - if (arg1 instanceof ZodSchema) { + if (this.isZodSchema(arg1)) { schema = arg1; options = arg2 ?? {}; - includeHeaders = - (arg2 as { includeHeaders?: boolean })?.includeHeaders ?? false; } else { options = arg1 ?? {}; - includeHeaders = - (arg1 as { includeHeaders?: boolean })?.includeHeaders ?? false; } + includeHeaders = options.includeHeaders ?? false; + const headers = new Headers( this.cleanHeaders({ ...this.defaultOptions.headers, ...options.headers, - }), + }) ); - const isJson = !(body instanceof FormData) && !headers.has("Content-Type"); - const bodyPayload = JSON.stringify(body); + const isFormData = body instanceof FormData; + const isJson = !isFormData && !headers.has("Content-Type"); if (isJson) { headers.set("Content-Type", "application/json"); @@ -325,7 +325,7 @@ export class Api { const mergedOptions: RequestInit = { ...options, method: "POST", - body: bodyPayload, + body: isJson ? JSON.stringify(body) : (body as BodyInit), headers, }; @@ -333,38 +333,41 @@ export class Api { T | { data: T; headers: Headers } >; } + isZodSchema(arg: any): arg is ZodSchema { + return typeof arg?.safeParse === "function"; + } async put( endpoint: string, body: BodyInit | Record, - options?: RequestInit, + options?: RequestInit ): Promise; async put( endpoint: string, body: BodyInit | Record, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async put( endpoint: string, body: BodyInit | Record, schema: ZodSchema, - options?: RequestInit, + options?: RequestInit ): Promise; async put( endpoint: string, body: BodyInit | Record, schema: ZodSchema, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async put( endpoint: string, body: BodyInit | Record, arg1?: ZodSchema | RequestInit, - arg2?: RequestInit, + arg2?: RequestInit ): Promise { let schema: ZodSchema | undefined; let options: RequestInit = {}; @@ -385,7 +388,7 @@ export class Api { this.cleanHeaders({ ...this.defaultOptions.headers, ...options.headers, - }), + }) ); const isJson = !(body instanceof FormData) && !headers.has("Content-Type"); @@ -411,25 +414,25 @@ export class Api { async delete( endpoint: string, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async delete( endpoint: string, schema: ZodSchema, - options?: RequestInit, + options?: RequestInit ): Promise; async delete( endpoint: string, schema: ZodSchema, - options: RequestInit & { includeHeaders: true }, + options: RequestInit & { includeHeaders: true } ): Promise<{ data: T; headers: Headers }>; async delete( endpoint: string, arg1?: ZodSchema | RequestInit, - arg2?: RequestInit, + arg2?: RequestInit ): Promise { let schema: ZodSchema | undefined; let options: RequestInit = {}; @@ -449,12 +452,12 @@ export class Api { return this.request( endpoint, { ...options, method: "DELETE" }, - includeHeaders, + includeHeaders ) as Promise; } private cleanHeaders( - headers: Record, + headers: Record ): Record { return Object.fromEntries( Object.entries(headers) @@ -462,7 +465,7 @@ export class Api { .map(([key, value]) => [ key, String(value as NonNullable), - ]), + ]) ); } } diff --git a/src/app/(pages)/(protected)/create/form.tsx b/src/app/(pages)/(protected)/create/form.tsx index 3620343..4cc6c51 100644 --- a/src/app/(pages)/(protected)/create/form.tsx +++ b/src/app/(pages)/(protected)/create/form.tsx @@ -10,10 +10,12 @@ import { Textarea, TagInput, Submit, + Dropdown, } from "@/context/form"; import { ingredientParser, instructionParser } from "@/context/form/fn"; import { z } from "zod"; import { ApiError } from "@/api/error"; +import { createRecipe } from "./server"; const err = (message: string) => `${message}. Please check and try again.`; @@ -66,9 +68,11 @@ export const recipeSchema = z.object({ err("Duration exceeds 24 hours—double-check this!") ) .default(0), - cookingMethod: z.coerce - .string() - .min(1, err("Specify a cooking method (e.g., 'Bake' or 'Stir-fry')")), + cookingMethod: z.array( + z + .string() + .min(1, err("Specify a cooking method (e.g., 'Bake' or 'Stir-fry')")) + ), ingredients: z .array(ingredientSchema) .min(1, err("Your recipe needs at least one ingredient!")) @@ -87,7 +91,7 @@ export const recipeSchema = z.object({ .array(z.string().min(1, err("Tags can't be empty (e.g., 'Vegetarian')"))) .optional() .default([]), - draft: z.boolean().default(false), + draft: z.coerce.boolean().default(false), }); export const CreateRecipeForm = ({ @@ -98,75 +102,73 @@ export const CreateRecipeForm = ({ const createRecipeAction = createAction(recipeSchema, async (_, formData) => { try { console.log("Form data:", formData); - // const formDataToSend = new FormData(); - // // Handle arrays properly - // const arraysToProcess = { - // ingredients: formData.ingredients, - // instructions: formData.instructions, - // tags: formData.tags, - // }; + const formDataToSend = new FormData(); - // Object.entries(arraysToProcess).forEach(([key, value]) => { - // if (Array.isArray(value)) { - // value.forEach((item, index) => { - // if (key === "ingredients") { - // if (typeof item === "object" && "value" in item) { - // formDataToSend.append(`${key}[${index}][value]`, item.value); - // } - // if (typeof item === "object" && "quantity" in item) { - // formDataToSend.append( - // `${key}[${index}][quantity]`, - // String(item.quantity) - // ); - // } - // if (typeof item === "object" && "unitsOfMeasurement" in item) { - // formDataToSend.append( - // `${key}[${index}][unitsOfMeasurement]`, - // item.unitsOfMeasurement - // ); - // } - // } else { - // // Handle simple arrays (instructions/tags) - // formDataToSend.append( - // `${key}[]`, - // typeof item === "object" ? JSON.stringify(item) : item - // ); - // } - // }); - // } - // }); + // // Handle arrays properly + const arraysToProcess = { + ingredients: formData.ingredients, + instructions: formData.instructions, + cookingMethod: formData.cookingMethod, + tags: formData.tags, + }; - // // Handle draft boolean - // formDataToSend.append("draft", formData.draft ? "true" : "false"); + Object.entries(arraysToProcess).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((item, index) => { + if (key === "ingredients") { + if (typeof item === "object" && "value" in item) { + formDataToSend.append( + `${key}[${index}][value]`, + item.value.trim() + ); + } + if (typeof item === "object" && "quantity" in item) { + formDataToSend.append( + `${key}[${index}][quantity]`, + String(item.quantity).trim() + ); + } + if (typeof item === "object" && "unitsOfMeasurement" in item) { + formDataToSend.append( + `${key}[${index}][unitsOfMeasurement]`, + item.unitsOfMeasurement.trim() + ); + } + } else { + formDataToSend.append( + `${key}[${index}]`, + typeof item === "object" ? JSON.stringify(item) : item.trim() + ); + } + }); + } + }); - // // Handle file upload - // if (formData.src instanceof File) { - // formDataToSend.append("src", formData.src); - // } + // Handle draft boolean + formDataToSend.append("draft", formData.draft ? "true" : "false"); - // // Add simple fields - // const simpleFields = [ - // "title", - // "description", - // "portions", - // "cookingDuration", - // "cookingMethod", - // ]; - // simpleFields.forEach((field) => { - // formDataToSend.append( - // field, - // String(formData[field as keyof typeof formData]) - // ); - // }); + // Handle file upload + if (formData.src instanceof File) { + formDataToSend.append("src", formData.src); + } - // const response = await API.post("/recipes", formDataToSend, { - // headers: { - // Authorization: `Bearer ${sessionToken}`, - // "Content-Type": "multipart/form-data", - // }, - // }); + // Add simple fields + const simpleFields = [ + "title", + "description", + "portions", + "cookingDuration", + ]; + simpleFields.forEach((field) => { + formDataToSend.append( + field, + String(formData[field as keyof typeof formData]) + ); + }); + console.log("formData to send: ", formDataToSend); + createRecipe(formDataToSend, sessionToken); return { success: true, message: "Recipe created successfully!", @@ -191,6 +193,43 @@ export const CreateRecipeForm = ({ }; } }); + enum UnitsOfMeasurement { + MG = "mg", + G = "g", + KG = "kg", + ML = "ml", + L = "l", + CUP = "cup", + TBSP = "tbsp", + TSP = "tsp", + OZ = "oz", + LB = "lb", + PINT = "pint", + QT = "qt", + GAL = "gal", + PN = "pinch", + PC = "piece", + SLICE = "slice", + STALK = "stalk", + CLOVES = "cloves", + BUNCH = "bunch", + CUBE = "cube", + BOTTLE = "bottle", + CAN = "can", + JAR = "jar", + BOWL = "bowl", + STICK = "stick", + TABLE = "table", + } + enum CookingMethod { + FryingPan = "Frying Pan", + Oven = "Oven", + Microwave = "Microwave", + AirFryer = "Air Fryer", + Blender = "Blender", + Boil = "Boil", + None = "None", + } return ( - - tag.length > 0} - /> -
+ /> */} + {/*
Save as draft -
+
*/} Create Recipe
@@ -255,15 +296,14 @@ export const CreateRecipeForm = ({ name={`ingredients.${index}.quantity`} type="number" placeholder="Qty" - className="w-20" onPaste={handlePaste} min={0} step="0.1" /> - (
- {index + 1}.