diff --git a/public/image-630x500.jpeg b/public/image-630x500.jpeg new file mode 100644 index 00000000..c07c25ed Binary files /dev/null and b/public/image-630x500.jpeg differ diff --git a/src/components/Navigation/Header.tsx b/src/components/Navigation/Header.tsx index 08cd5541..62b72949 100644 --- a/src/components/Navigation/Header.tsx +++ b/src/components/Navigation/Header.tsx @@ -63,7 +63,6 @@ const Header = () => { } else { setLoaded(true); } - console.log(currentUser); const pathname = router.pathname; const tabNames = Object.keys(tabLinks); const index = tabNames.findIndex((name) => tabLinks[name] === pathname); diff --git a/src/pages/games/create.tsx b/src/pages/games/create.tsx index e23f473e..8fb8df2c 100644 --- a/src/pages/games/create.tsx +++ b/src/pages/games/create.tsx @@ -2,13 +2,12 @@ import React from "react"; import pageAccessHOC from "@/components/HOC/PageAccess"; import { z } from "zod"; import cn from "classnames"; - +import { Upload } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { TextArea } from "@/components/ui/textarea"; -import { AlertTriangleIcon, MoveLeft, Plus } from "lucide-react"; - -import { useState, useEffect } from "react"; +import { AlertTriangleIcon, MoveLeft, Plus, X } from "lucide-react"; +import { useState, useEffect, useRef } from "react"; import { useRouter } from "next/router"; import ThemeSelect from "@/components/Themes/ThemeSelect"; import TagSelect from "@/components/Tags/TagSelect"; @@ -28,9 +27,11 @@ import axios from "axios"; const NAME_FORM_KEY = "name"; const TRAILER_FORM_KEY = "videoTrailer"; const DESCR_FORM_KEY = "description"; +const IMAGE_FORM_KEY = "image"; export const createGameSchema = z.object({ name: z.string().min(3, "Title must be at least 3 characters"), + image: z.string().min(1, "Image is required"), videoTrailer: z.string().url("Not a valid URL").or(z.literal("")), description: z.string().min(1, "Description is required"), }); @@ -106,7 +107,7 @@ async function uploadBuildFiles(gameId: string, files: Map) { function CreateGame() { const router = useRouter(); - + const hiddenImagePreviewRef = useRef(null); const [themes, setThemes] = useState[]>([]); const [selectedThemes, setSelectedThemes] = useState[]>([]); @@ -131,6 +132,7 @@ function CreateGame() { const [codeFile, setCodeFile] = useState(null); const [frameworkFile, setFrameworkFile] = useState(null); + const [imagePreviewFile, setImagePreviewFile] = useState(null); const [fileValidationError, setFileValidationError] = useState< string | undefined >(undefined); @@ -183,6 +185,7 @@ function CreateGame() { Record, string | undefined> >({ name: undefined, + image: undefined, videoTrailer: undefined, description: undefined, }); @@ -244,13 +247,47 @@ function CreateGame() { setFileValidationError(undefined); } } - + if (imagePreviewFile) { + if ( + imagePreviewFile.type !== "image/png" && + imagePreviewFile.type !== "image/jpg" && + imagePreviewFile.type !== "image/jpeg" + ) { + setValidationErrors((prevValidationErrors) => ({ + ...prevValidationErrors, + image: "Invalid Image: Only PNG, JPG, or JPEG permitted.", + })); + return; + } + const img = new Image(); + img.src = URL.createObjectURL(imagePreviewFile); + img.onload = () => { + const naturalWidth = img.naturalWidth; + const naturalHeight = img.naturalHeight; + URL.revokeObjectURL(img.src); + if (naturalWidth !== 630 || naturalHeight !== 500) { + setValidationErrors((prevValidationErrors) => ({ + ...prevValidationErrors, + image: "Image must have dimensions 630x500 pixels.", + })); + return; + } + }; + img.onerror = () => { + setValidationErrors((prevValidationErrors) => ({ + ...prevValidationErrors, + image: "Image failed to load", + })); + return; + }; + } const formData = new FormData(e.currentTarget); const input = { name: formData.get(NAME_FORM_KEY), videoTrailer: formData.get(TRAILER_FORM_KEY), description: formData.get(DESCR_FORM_KEY), builds: builds, + image: imagePreviewFile ? "http://dummy-image-url.com" : "", // Temporary image themes: selectedThemes.map((theme) => theme._id), tags: [...selectedAccessibilityTags, ...selectedCustomTags].map( (tag) => tag._id, @@ -262,6 +299,7 @@ function CreateGame() { if (parse.success) { setValidationErrors({ name: undefined, + image: undefined, videoTrailer: undefined, description: undefined, }); @@ -294,8 +332,10 @@ function CreateGame() { } else { setSubmitting(false); const errors = parse.error.formErrors.fieldErrors; + console.log(errors); setValidationErrors({ name: errors.name?.at(0), + image: errors.image?.at(0), videoTrailer: errors.videoTrailer?.at(0), description: errors.description?.at(0), }); @@ -387,9 +427,67 @@ function CreateGame() { "h-12", )} disabled={submitting} + onChange={() => { + setValidationErrors({ ...validationErrors, name: undefined }); + }} /> +
+ +
+ + {imagePreviewFile ? ( +
+

{imagePreviewFile.name}

+ { + setImagePreviewFile(null); + }} + /> +
+ ) : null} +
+ ) => { + if ( + event.target.files === null || + event.target.files.length === 0 + ) + return; + setImagePreviewFile(event.target.files[0]); + }} + > +
@@ -445,6 +549,12 @@ function CreateGame() { ? "border-red-500 focus-visible:ring-red-500" : "border-input-border focus:border-blue-primary" } + onChange={() => { + setValidationErrors({ + ...validationErrors, + description: undefined, + }); + }} /> @@ -488,7 +598,9 @@ function CreateGame() { ? validationErrors.name : validationErrors.videoTrailer ? validationErrors.videoTrailer - : "All required fields need to be filled."} + : validationErrors.image + ? validationErrors.image + : "All required fields need to be filled."}

)} diff --git a/src/server/db/actions/__mocks__/GameAction.ts b/src/server/db/actions/__mocks__/GameAction.ts index 8a61f3e9..998b7481 100644 --- a/src/server/db/actions/__mocks__/GameAction.ts +++ b/src/server/db/actions/__mocks__/GameAction.ts @@ -20,6 +20,7 @@ function createRandomGame(): GamesFilterOutput[number] { lesson: faker.internet.url(), parentingGuide: faker.internet.url(), answerKey: faker.internet.url(), + image: faker.internet.url(), videoTrailer: faker.internet.url(), builds: Array.from({ length: numBuilds }).map(() => { return { diff --git a/src/server/db/models/GameModel.ts b/src/server/db/models/GameModel.ts index 5bbc456c..1dc50592 100644 --- a/src/server/db/models/GameModel.ts +++ b/src/server/db/models/GameModel.ts @@ -50,6 +50,7 @@ const GameSchema = new Schema( answerKey: { type: String }, videoTrailer: { type: String }, preview: { type: Boolean, required: true }, + image: { type: String, required: true }, }, { versionKey: false }, ); diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index c9cbe9f0..391561a0 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -103,6 +103,7 @@ export const gameSchema = z.object({ lesson: z.string().url().optional(), parentingGuide: z.string().url().optional(), answerKey: z.string().url().optional(), + image: z.string().min(1, "Image is required"), videoTrailer: z.preprocess( (val) => (val === "" ? undefined : val), z.string().url().optional(),