From 811bd3f0eefb093623d4d2fab5e48d235aaaa673 Mon Sep 17 00:00:00 2001 From: Angelina Wang Date: Wed, 5 Feb 2025 13:41:37 -0800 Subject: [PATCH 01/26] created branch. --- client/.env.example | 11 -- client/src/components/forms/createArticle.tsx | 100 ++++++++++++++++++ client/src/components/resources/Resources.jsx | 5 + server/routes/articles.ts | 19 ++++ 4 files changed, 124 insertions(+), 11 deletions(-) delete mode 100644 client/.env.example create mode 100644 client/src/components/forms/createArticle.tsx diff --git a/client/.env.example b/client/.env.example deleted file mode 100644 index 02b3a30..0000000 --- a/client/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -VITE_FIREBASE_APIKEY=VITE_FIREBASE_APIKEY -VITE_FIREBASE_AUTHDOMAIN=VITE_FIREBASE_AUTHDOMAIN -VITE_FIREBASE_PROJECTID=VITE_FIREBASE_PROJECTID -VITE_FIREBASE_STORAGEBUCKET=VITE_FIREBASE_STORAGEBUCKET -VITE_FIREBASE_MESSAGINGSENDERID=VITE_FIREBASE_MESSAGINGSENDERID -VITE_FIREBASE_APPID=VITE_FIREBASE_APPID -VITE_FIREBASE_MEASUREMENTID=VITE_FIREBASE_MEASUREMENTID - -VITE_FRONTEND_HOSTNAME=localhost:3000 -VITE_BACKEND_HOSTNAME=http://localhost:3001 -VITE_ADMIN_EMAIL="something@gmail.com" \ No newline at end of file diff --git a/client/src/components/forms/createArticle.tsx b/client/src/components/forms/createArticle.tsx new file mode 100644 index 0000000..e8492fd --- /dev/null +++ b/client/src/components/forms/createArticle.tsx @@ -0,0 +1,100 @@ +import { + Box, + Button, + FormControl, + FormLabel, + FormErrorMessage, + Input, + } from '@chakra-ui/react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const createArticleSchema = z.object({ + photo: z.string(), + link: z.string().url("Please enter a valid url."), + tag: z.string().min(1, "You must include at least one tag."), + textInput: z.string().min(1, "Please write a description of the article."), + title: z.string().min(1, "Your article must include a title.") +}); + +type ArticleFormValues = z.infer; + +const CreateArticle = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(createArticleSchema), + mode: "onBlur", + }); + + const onSubmit = async (data: ArticleFormValues) => { + try { + const response = await fetch("/articles/search/:title", { + method: "POST", + body: JSON.stringify(data), + }); + + const result = await response.json(); + console.log("Successfully submitted article.") + alert("Article submitted successfully!") + } catch (error) { + console.log("Error submitting article:", error); + alert("Failed to submit article."); + } + }; + + return ( + +
+ + Select media to upload. + + + + + Select tags for media. +
+ + + +
+
+ + Add a title. + + Title is required. + + + {/* insert review section */} +
+
+ ) +} + +export default CreateArticle; \ No newline at end of file diff --git a/client/src/components/resources/Resources.jsx b/client/src/components/resources/Resources.jsx index a42ed4a..ac667f1 100644 --- a/client/src/components/resources/Resources.jsx +++ b/client/src/components/resources/Resources.jsx @@ -3,9 +3,11 @@ import { VideoCard } from "./VideoCard"; import { NewsCard } from "./NewsCard"; import { Button, Flex, Text, Box } from "@chakra-ui/react"; import { useBackendContext } from "../../contexts/hooks/useBackendContext"; +import CreateArticle from "../../components/forms/createArticle.tsx"; export const Resources = () => { + const [showForm, setShowForm] = useState(false); const { backend } = useBackendContext() const [videos, setVideos] = useState([]); const [news, setNews] = useState([]); @@ -80,6 +82,9 @@ export const Resources = () => { ))} + + + {showForm && } ); }; diff --git a/server/routes/articles.ts b/server/routes/articles.ts index 60c8735..cdc3891 100644 --- a/server/routes/articles.ts +++ b/server/routes/articles.ts @@ -71,6 +71,25 @@ articlesRouter.post("/", async (req, res) => { } }); +// GET /articles/search/:title +articlesRouter.get("/search/:title", async (req, res) => { + try { + const { title } = req.body; + + if (!title) { + return res.status(400).json({ error: "Missing article title."}); + } + + const rows = await db.query( + "SELECT * FROM articles WHERE title LIKE $1", [title] + ) + + res.status(201).json(keysToCamel(rows[0] as Article)); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}) + // PUT /articles/:id articlesRouter.put("/:id", async (req, res) => { try { From 8f96089c4988189b0c04726865b814dca4688768 Mon Sep 17 00:00:00 2001 From: Angelina Wang Date: Wed, 5 Feb 2025 20:33:24 -0800 Subject: [PATCH 02/26] finished resource ui --- client/src/components/forms/createArticle.tsx | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/client/src/components/forms/createArticle.tsx b/client/src/components/forms/createArticle.tsx index e8492fd..5b2c0aa 100644 --- a/client/src/components/forms/createArticle.tsx +++ b/client/src/components/forms/createArticle.tsx @@ -1,20 +1,24 @@ import { Box, Button, + Checkbox, FormControl, FormLabel, FormErrorMessage, Input, + Stack, + VStack, } from '@chakra-ui/react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from "react-hook-form"; +import { useForm, Controller } from "react-hook-form"; +import { useState } from "react"; import { z } from "zod"; const createArticleSchema = z.object({ photo: z.string(), link: z.string().url("Please enter a valid url."), - tag: z.string().min(1, "You must include at least one tag."), + tag: z.array(z.string()).min(1, "You must include at least one tag."), textInput: z.string().min(1, "Please write a description of the article."), title: z.string().min(1, "Your article must include a title.") }); @@ -25,6 +29,7 @@ const CreateArticle = () => { const { register, handleSubmit, + setValue, formState: { errors }, } = useForm({ resolver: zodResolver(createArticleSchema), @@ -47,14 +52,44 @@ const CreateArticle = () => { } }; + const [selectTags, setSelectTags] = useState([]); + + const tags = ["Ballet", "Classical", "Custom"]; + + const handleTags = (tag: string) => { + let newTags; + if (selectTags.includes(tag)) { + newTags = selectTags.filter((t) => t !== tag); + } else { + newTags = [...selectTags, tag] + } + + setSelectTags(newTags); + setValue("tag", newTags, {shouldValidate: true}); + } + return ( +
- + Select media to upload. + + {errors.photo?.message?.toString()} + + + { isRequired autoComplete="link" > + {errors.link?.message?.toString()} - + Select tags for media. -
- - - -
+ + {tags.map((tag) => ( + + ))} + + {errors.tag?.message}
- + Add a title. { isRequired autoComplete="title" > - Title is required. + {errors.title?.message?.toString()} {/* insert review section */}
+
) } From 5bee44d21830b3861819082a4d626ce39c6a6d9b Mon Sep 17 00:00:00 2001 From: Angelina Wang Date: Wed, 5 Feb 2025 22:30:53 -0800 Subject: [PATCH 03/26] Started backend. --- client/src/components/forms/createArticle.tsx | 36 ++++++++++--------- server/routes/articles.ts | 13 ++++--- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/client/src/components/forms/createArticle.tsx b/client/src/components/forms/createArticle.tsx index 5b2c0aa..9ed01f3 100644 --- a/client/src/components/forms/createArticle.tsx +++ b/client/src/components/forms/createArticle.tsx @@ -16,10 +16,10 @@ import { useState } from "react"; import { z } from "zod"; const createArticleSchema = z.object({ - photo: z.string(), - link: z.string().url("Please enter a valid url."), + s3_url: z.string(), + media_url: z.string().url("Please enter a valid URL."), tag: z.array(z.string()).min(1, "You must include at least one tag."), - textInput: z.string().min(1, "Please write a description of the article."), + description: z.string().min(1, "Please write a description of the article."), title: z.string().min(1, "Your article must include a title.") }); @@ -38,12 +38,16 @@ const CreateArticle = () => { const onSubmit = async (data: ArticleFormValues) => { try { - const response = await fetch("/articles/search/:title", { + const response = await fetch("/articles", { method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); + + if (!response.ok) { + throw new Error("Failed to submit article."); + } - const result = await response.json(); console.log("Successfully submitted article.") alert("Article submitted successfully!") } catch (error) { @@ -75,31 +79,31 @@ const CreateArticle = () => { >
- + Select media to upload. - {errors.photo?.message?.toString()} + {errors.s3_url?.message?.toString()} - + - {errors.link?.message?.toString()} + {errors.media_url?.message?.toString()} Select tags for media. diff --git a/server/routes/articles.ts b/server/routes/articles.ts index cdc3891..b3fb2fa 100644 --- a/server/routes/articles.ts +++ b/server/routes/articles.ts @@ -74,17 +74,22 @@ articlesRouter.post("/", async (req, res) => { // GET /articles/search/:title articlesRouter.get("/search/:title", async (req, res) => { try { - const { title } = req.body; + const { title } = req.params; if (!title) { return res.status(400).json({ error: "Missing article title."}); } const rows = await db.query( - "SELECT * FROM articles WHERE title LIKE $1", [title] - ) + "SELECT * FROM articles WHERE title LIKE $1", + [`%${title}%`] + ); - res.status(201).json(keysToCamel(rows[0] as Article)); + if (rows.length === 0) { + return res.status(404).json({ error: "No articles found."}); + } + + res.status(200).json(keysToCamel(keysToCamel(rows) as Article[])); } catch (error) { res.status(500).json({ error: error.message }); } From ea58acc0858ee68fc4a4bd4691d43c12d06d3d17 Mon Sep 17 00:00:00 2001 From: Alex Espejo Date: Wed, 5 Feb 2025 23:43:46 -0800 Subject: [PATCH 04/26] create video --- client/src/components/forms/createVideo.jsx | 238 ++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 client/src/components/forms/createVideo.jsx diff --git a/client/src/components/forms/createVideo.jsx b/client/src/components/forms/createVideo.jsx new file mode 100644 index 0000000..4a1daf9 --- /dev/null +++ b/client/src/components/forms/createVideo.jsx @@ -0,0 +1,238 @@ +import { useEffect, useRef, useState } from "react"; + +import { + Box, + Button, + FormControl, + FormErrorMessage, + FormLabel, + Image, + Input, + Select, + Stack, + Textarea, + VisuallyHidden, +} from "@chakra-ui/react"; + +import { useBackendContext } from "../../contexts/hooks/useBackendContext"; + +function CreateVideo() { + const fileInputRef = useRef(null); + const videoInputRef = useRef(null); + + const [videoData, setVideoData] = useState({ + title: "", + s3_url: "", + description: "", + media_url: "", + class_id: "", + }); + + const { backend } = useBackendContext(); + + const [activeButton, setActiveButton] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [videoFile, setVideoFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [classes, setClasses] = useState([]); + + const fetchClasses = async () => { + try { + const classesResponse = await backend.get("/classes"); + setClasses(classesResponse.data); + } catch (error) { + console.error("Error fetching classes:", error); + } + }; + + const toggleButton = (buttonName) => { + setActiveButton((currentActive) => + currentActive === buttonName ? null : buttonName + ); + }; + + const handleFileUploadClick = () => { + fileInputRef.current.click(); + }; + const handleVideoUploadClick = () => { + videoInputRef.current.click(); + }; + + const handleFileChange = (event) => { + const file = event.target.files[0]; + if (file) { + setSelectedFile(file); + setVideoData({ + ...videoData, + media_url: `placeholder.bcIdontthinkthiswasinthescopeofthisticket/${file?.name}`, + }); + + const fileReader = new FileReader(); + fileReader.onload = () => { + setPreviewUrl(fileReader.result); + }; + fileReader.readAsDataURL(file); + } + }; + + const handleVideoFileChange = (event) => { + const file = event.target.files[0]; + if (file) { + setVideoFile(file); + setVideoData({ + ...videoData, + s3_url: `placeholder.bcIdontthinkthiswasinthescopeofthisticket/${file?.name}`, + }); + console.log("File selected:", file.name); + } + }; + + useEffect(() => { + fetchClasses(); + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl, classes]); + + return ( + + + {!previewUrl ? ( + + Upload Image + + + + + + ) : ( + + Preview setPreviewUrl(null)} + /> + + )} + + Upload Video + + + + + + + + + + Video Title + + setVideoData({ ...videoData, title: e.target.value }) + } + /> + + + Description +