Skip to content

Commit

Permalink
Merge pull request #105 from GTBitsOfGood/ankith/image-preview
Browse files Browse the repository at this point in the history
Ankith/image preview
  • Loading branch information
03hchen authored Apr 7, 2024
2 parents c7bb682 + 55f8d9e commit 24f00f1
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 8 deletions.
Binary file added public/image-630x500.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion src/components/Navigation/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
126 changes: 119 additions & 7 deletions src/pages/games/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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"),
});
Expand Down Expand Up @@ -106,7 +107,7 @@ async function uploadBuildFiles(gameId: string, files: Map<string, File>) {

function CreateGame() {
const router = useRouter();

const hiddenImagePreviewRef = useRef<HTMLInputElement>(null);
const [themes, setThemes] = useState<ExtendId<ITheme>[]>([]);
const [selectedThemes, setSelectedThemes] = useState<ExtendId<ITheme>[]>([]);

Expand All @@ -131,6 +132,7 @@ function CreateGame() {
const [codeFile, setCodeFile] = useState<null | File>(null);
const [frameworkFile, setFrameworkFile] = useState<null | File>(null);

const [imagePreviewFile, setImagePreviewFile] = useState<null | File>(null);
const [fileValidationError, setFileValidationError] = useState<
string | undefined
>(undefined);
Expand Down Expand Up @@ -183,6 +185,7 @@ function CreateGame() {
Record<keyof z.input<typeof createGameSchema>, string | undefined>
>({
name: undefined,
image: undefined,
videoTrailer: undefined,
description: undefined,
});
Expand Down Expand Up @@ -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,
Expand All @@ -262,6 +299,7 @@ function CreateGame() {
if (parse.success) {
setValidationErrors({
name: undefined,
image: undefined,
videoTrailer: undefined,
description: undefined,
});
Expand Down Expand Up @@ -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),
});
Expand Down Expand Up @@ -387,9 +427,67 @@ function CreateGame() {
"h-12",
)}
disabled={submitting}
onChange={() => {
setValidationErrors({ ...validationErrors, name: undefined });
}}
/>
</div>

<div className="relative flex w-full flex-col gap-3 md:w-2/5">
<label htmlFor={IMAGE_FORM_KEY} className="text-xl font-semibold">
Image Preview
<span className="text-orange-primary">*</span>
</label>
<div className="flex gap-6">
<Button
name={IMAGE_FORM_KEY}
variant="upload"
className={cn(
validationErrors.image
? "border-red-500 focus-visible:ring-red-500"
: "",
"flex h-12 w-32 flex-row gap-3 self-start",
)}
type="button"
onClick={() => {
setValidationErrors({ ...validationErrors, image: undefined });
if (hiddenImagePreviewRef.current !== null) {
hiddenImagePreviewRef.current.click();
}
}}
>
<Upload className="h-6 w-6" />
<p>Upload</p>
</Button>
{imagePreviewFile ? (
<div className="flex flex-row items-center gap-3">
<p>{imagePreviewFile.name}</p>
<X
className="cursor-pointer text-orange-primary"
type="button"
size={18}
onClick={() => {
setImagePreviewFile(null);
}}
/>
</div>
) : null}
</div>
<Input
type="file"
ref={hiddenImagePreviewRef}
accept=".jpg,.jpeg,.png"
className="hidden"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if (
event.target.files === null ||
event.target.files.length === 0
)
return;
setImagePreviewFile(event.target.files[0]);
}}
></Input>
</div>
<div className="relative flex w-full flex-col gap-3 md:w-2/3">
<label className="text-xl font-semibold">
Game Build
Expand Down Expand Up @@ -429,6 +527,12 @@ function CreateGame() {
: "border-input-border focus:border-blue-primary",
"h-12",
)}
onChange={() => {
setValidationErrors({
...validationErrors,
videoTrailer: undefined,
});
}}
/>
</div>

Expand All @@ -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,
});
}}
/>
</div>

Expand Down Expand Up @@ -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."}
</p>
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions src/server/db/actions/__mocks__/GameAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/server/db/models/GameModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const GameSchema = new Schema<IGame>(
answerKey: { type: String },
videoTrailer: { type: String },
preview: { type: Boolean, required: true },
image: { type: String, required: true },
},
{ versionKey: false },
);
Expand Down
1 change: 1 addition & 0 deletions src/utils/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down

0 comments on commit 24f00f1

Please sign in to comment.