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.jsx b/client/src/components/forms/createArticle.jsx
new file mode 100644
index 0000000..2b2c559
--- /dev/null
+++ b/client/src/components/forms/createArticle.jsx
@@ -0,0 +1,202 @@
+import {
+ Box,
+ Button,
+ FormControl,
+ FormLabel,
+ FormErrorMessage,
+ Input,
+ Stack,
+ VStack,
+ } from '@chakra-ui/react';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from "react-hook-form";
+import { useRef, useState } from "react";
+import { z } from "zod";
+
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+
+const createArticleSchema = z.object({
+ s3_url: z.string(),
+ media_url: z.string().url("Please enter a valid URL."),
+ description: z.string().min(1, "Please write a description of the article."),
+ title: z.string().min(1, "Your article must include a title.")
+});
+
+const CreateArticle = () => {
+ const { backend } = useBackendContext();
+ const [fileName, setFileName] = useState("");
+
+ const [activeButton, setActiveButton] = useState(null);
+ const toggleButton = (tag) => {
+ setActiveButton(activeButton === tag ? null : tag);
+ }
+
+ const fileInputRef = useRef(null);
+
+ const handleFileChange = async (event) => {
+ if (event.target.files && event.target.files.length > 0) {
+ const selectedFile = event.target.files[0];
+ setFileName(selectedFile.name);
+
+ const formData = new FormData();
+ formData.append("file", selectedFile);
+ }
+
+ const testURL = "https://wawawa.com/ok.jpg";
+ setValue("s3_url", testURL, { shouldValidate: true });
+ }
+
+ const {
+ register,
+ handleSubmit,
+ setValue,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(createArticleSchema),
+ mode: "onBlur",
+ });
+
+ const postArticle = async (data) => {
+ console.log("Submitting article with data:", data);
+
+ try {
+ const response = await backend.post("/articles", {
+ s3_url: data.s3_url ?? "",
+ media_url: data.media_url ?? "",
+ description: data.title ?? "",
+ });
+
+ if (!response) {
+ throw new Error("Failed to submit article.");
+ }
+
+ console.log("Successfully submitted article.")
+ alert("Article submitted successfully!")
+ } catch (error) {
+ console.log("Error submitting article:", error);
+ alert("Failed to submit article.");
+ }
+ };
+
+ const [selectTags, setSelectTags] = useState([]);
+
+ const tags = ["Ballet", "Classical", "Custom"];
+
+ const handleTags = (tag) => {
+ let newTags;
+ if (selectTags.includes(tag)) {
+ newTags = selectTags.filter((t) => t !== tag);
+ } else {
+ newTags = [...selectTags, tag]
+ }
+
+ setSelectTags(newTags);
+ setValue("tag", newTags, {shouldValidate: true});
+ }
+
+ return (
+
+
+
+
+
+ )
+}
+
+export default CreateArticle;
\ No newline at end of file
diff --git a/client/src/components/forms/createVideo.jsx b/client/src/components/forms/createVideo.jsx
new file mode 100644
index 0000000..b69c490
--- /dev/null
+++ b/client/src/components/forms/createVideo.jsx
@@ -0,0 +1,295 @@
+import { useEffect, useRef, useState } from "react";
+
+import {
+ Box,
+ Button,
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ Image,
+ Input,
+ Select,
+ Stack,
+ Text,
+ Textarea,
+ VisuallyHidden,
+} from "@chakra-ui/react";
+
+import { useBackendContext } from "../../contexts/hooks/useBackendContext";
+import { VideoCard } from "../resources/VideoCard";
+
+function CreateVideo() {
+ const fileInputRef = useRef(null);
+
+ const [videoData, setVideoData] = useState({
+ title: "",
+ s3_url: "",
+ description: "",
+ media_url: "",
+ class_id: "",
+ });
+
+ const [errors, setErrors] = useState({
+ title: false,
+ description: false,
+ media_url: false,
+ class_id: false,
+ s3_url: false,
+ });
+
+ const { backend } = useBackendContext();
+
+ const [activeButton, setActiveButton] = useState(null);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+ const [classes, setClasses] = useState([]);
+
+ const validateForm = () => {
+ const newErrors = {
+ title: videoData.title.trim() === "",
+ description: videoData.description.trim() === "",
+ media_url: videoData.media_url.trim() === "",
+ class_id: videoData.class_id.trim() === "",
+ s3_url: videoData.s3_url.trim() === "",
+ };
+ setErrors(newErrors);
+
+ // Returns true if there are no errors
+ return (
+ !newErrors.title &&
+ !newErrors.description &&
+ !newErrors.media_url &&
+ !newErrors.class_id &&
+ !newErrors.s3_url
+ );
+ };
+
+ 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 handleFileChange = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ setSelectedFile(file);
+ setVideoData({
+ ...videoData,
+ s3_url: `/${file?.name}`,
+ });
+
+ const fileReader = new FileReader();
+ fileReader.onload = () => {
+ setPreviewUrl(fileReader.result);
+ };
+ fileReader.readAsDataURL(file);
+ }
+ };
+
+
+ const fetchS3URL = async () => {
+ try {
+ const URLResponse = await backend.get('/s3/url');
+ console.log(URLResponse);
+ return URLResponse.data.url;
+ } catch (error) {
+ console.error('Error fetching S3 URL:', error);
+ }
+ };
+
+ const handleSubmit = async () => {
+
+ const s3Url = await fetchS3URL();
+
+ const uploadResponse = await fetch(s3Url, {
+ method: "PUT",
+ body: selectedFile,
+ headers: {
+ "Content-Type": selectedFile.type,
+ },
+ });
+
+ if (!uploadResponse.ok) {
+ throw new Error("Failed to upload file");
+ }
+
+
+ setVideoData({...videoData, s3_url: s3Url});
+
+ console.log("valid", videoData);
+ if (validateForm()) {
+ const res = await backend.post("/classes-videos", {
+ title: videoData.title ?? "",
+ s3Url: s3Url ?? "",
+ description: videoData.description ?? "",
+ mediaUrl: videoData.media_url ?? "",
+ classId: videoData.class_id ?? "",
+ });
+ window.location.reload(); // hold on i need to set up a proper form lol
+ }
+ };
+
+ useEffect(() => {
+ fetchClasses();
+ return () => {
+ if (previewUrl) {
+ URL.revokeObjectURL(previewUrl);
+ }
+ };
+ }, [previewUrl]);
+
+ return (
+
+
+
+ Upload Image
+
+
+
+
+
+ {errors.s3_url && (
+ Image is Required
+ )}
+
+
+ Upload Video Embed URL
+
+
+ Video Title
+
+ setVideoData({ ...videoData, title: e.target.value })
+ }
+ />
+ {errors.title && (
+ Title is Required
+ )}
+
+
+ Description
+
+ // these don't do anything lol
+
+ Tag
+
+
+
+
+
+
+
+ Select Class
+
+ {errors.class_id && (
+ Class is Required
+ )}
+
+
+ Preview
+
+
+
+
+
+ );
+}
+
+export default CreateVideo;
diff --git a/client/src/components/resources/Resources.jsx b/client/src/components/resources/Resources.jsx
index 2dee2c9..b2f9333 100644
--- a/client/src/components/resources/Resources.jsx
+++ b/client/src/components/resources/Resources.jsx
@@ -1,91 +1,242 @@
+
import { useState, useEffect } from "react";
import { VideoCard } from "./VideoCard";
import { NewsCard } from "./NewsCard";
-import { UploadComponent } from "./UploadComponent";
-import { Button, Flex, Text, Box } from "@chakra-ui/react";
+import { Navbar } from "../navbar/Navbar.jsx";
+import {
+ Alert,
+ AlertIcon,
+ Box,
+ Button,
+ Flex,
+ HStack,
+ Input,
+ Tab,
+ TabIndicator,
+ TabList,
+ Tabs,
+ Text,
+} from "@chakra-ui/react";
+
+import CreateArticle from "../../components/forms/createArticle.jsx";
import { useBackendContext } from "../../contexts/hooks/useBackendContext";
-import { Navbar } from "../navbar/Navbar";
+import CreateVideo from "../forms/createVideo.jsx";
export const Resources = () => {
-
+ const [showAlert, setShowAlert] = useState(false);
const { backend } = useBackendContext();
+
const [videos, setVideos] = useState([]);
const [news, setNews] = useState([]);
+ const [resourceFilter, setResourceFilter] = useState(0);
+ const [filterTitle, setFilterTitle] = useState(null);
- const handleVideoButton = () => {
- console.log('Videos button has been pressed!');
- console.log(videos);
- };
+ const [postArticle, setPostArticle] = useState(false);
+ const [formType, setFormType] = useState(null);
- const handleNewsButton = () => {
- console.log('News button has been pressed!');
- console.log(news);
+ const searchResouce = async () => {
+ if (resourceFilter === 0) {
+ try {
+ const videoResponse = await backend.get(
+ `/classes-videos/search/${filterTitle}`
+ );
+ setVideos(videoResponse.data);
+ setShowAlert(false);
+ } catch (error) {
+ setShowAlert(true);
+ }
+ } else if (resourceFilter === "NEWS") {
+ try {
+ const newsResponse = await backend.get(`/articles/search/${title}`);
+ setNews(newsResponse.data);
+ } catch (error) {
+ console.error("Error fetching news:", error);
+ }
+ }
};
const fetchVideos = async () => {
try {
- const videoResponse = await backend.get('/classes-videos');
+ const videoResponse = await backend.get("/classes-videos");
setVideos(videoResponse.data);
} catch (error) {
- console.error('Error fetching videos:', error);
+ console.error("Error fetching videos:", error);
}
};
const fetchNews = async () => {
try {
- const newsResponse = await backend.get('/articles');
+ const newsResponse = await backend.get("/articles");
setNews(newsResponse.data);
} catch (error) {
- console.error('Error fetching news:', error);
+ console.error("Error fetching news:", error);
}
};
useEffect(() => {
fetchVideos(); // Fetch videos initially
- fetchNews(); // Fetch news initially
+ fetchNews(); // Fetch news initially
}, []);
return (
-
- Resources
-
-
-
-
-
- Videos
-
- {videos.map((video) => (
-
- ))}
+
+ {postArticle ? (
+
+ {formType === null ? (
+
+
+
+
+ ) : (
+ <>{formType === "VIDEO" ? : }>
+ )}
-
-
- News
-
- {news.map((newsItem) => (
-
+
+ Resources
+
+ setResourceFilter(index)}
+ >
+
+ Video
+ News
+
+
- ))}
-
-
-
+
+
+
+ setFilterTitle(e.target.value)}
+ />
+
+
+
+ {showAlert ? (
+
+
+ Sorry, we could not find any resources
+
+ ) : (
+ <>
+
+
+ Videos
+
+
+ {videos.map((video) => (
+
+ ))}
+
+
+
+
+ News
+
+
+ {news.map((newsItem) => (
+
+ ))}
+
+
+ >
+ )}
+ >
+ )}
+ {/* // uncentered button */}
+
-
+ {/* */}
);
};
diff --git a/server/routes/articles.ts b/server/routes/articles.ts
index 27dc804..9b165cf 100644
--- a/server/routes/articles.ts
+++ b/server/routes/articles.ts
@@ -71,6 +71,30 @@ articlesRouter.post("/", async (req, res) => {
}
});
+// GET /articles/search/:title
+articlesRouter.get("/search/:title", async (req, res) => {
+ try {
+ const { title } = req.params;
+
+ if (!title) {
+ return res.status(400).json({ error: "Missing article title." });
+ }
+
+ const rows = await db.query(
+ "SELECT * FROM articles WHERE description LIKE $1",
+ [`%${title}%`]
+ );
+
+ 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 });
+ }
+});
+
// PUT /articles/:id
articlesRouter.put("/:id", async (req, res) => {
try {
diff --git a/server/routes/class_videos.js b/server/routes/class_videos.js
index 2756775..8f4a9d7 100644
--- a/server/routes/class_videos.js
+++ b/server/routes/class_videos.js
@@ -1,4 +1,5 @@
import express from "express";
+
import { keysToCamel } from "../common/utils";
import { db } from "../db/db-pgp";
@@ -9,7 +10,9 @@ classVideosRouter.get("/:id", async (req, res) => {
try {
const { id } = req.params;
- const data = await db.query(`SELECT * FROM class_videos WHERE id = $1`, [id]);
+ const data = await db.query(`SELECT * FROM class_videos WHERE id = $1`, [
+ id,
+ ]);
res.status(200).json(keysToCamel(data));
} catch (err) {
@@ -30,7 +33,7 @@ classVideosRouter.get("/", async (req, res) => {
classVideosRouter.post("/", async (req, res) => {
try {
const { title, s3Url, description, mediaUrl, classId } = req.body;
-
+
const postData = await db.query(
`INSERT INTO class_videos (title, s3_url, description, media_url, class_id)
VALUES ($1, $2, $3, $4, $5) RETURNING id, title, s3_url, description, media_url, class_id;`,
@@ -66,23 +69,46 @@ classVideosRouter.put("/:id", async (req, res) => {
} else {
res.status(400).send("Video with provided id not found");
}
- }
- catch (err) {
+ } catch (err) {
res.status(500).send(err.message);
}
});
-classVideosRouter.delete('/:id', async (req, res) => {
+classVideosRouter.delete("/:id", async (req, res) => {
try {
const { id } = req.params;
- const deletedClassVideos = await db.query(`DELETE FROM class_videos WHERE id = $1 RETURNING *;`, [id]);
+ const deletedClassVideos = await db.query(
+ `DELETE FROM class_videos WHERE id = $1 RETURNING *;`,
+ [id]
+ );
res.status(200).send(keysToCamel(deletedClassVideos));
} catch (err) {
res.status(500).send(err.message);
}
});
-
+classVideosRouter.get("/search/:title", async (req, res) => {
+ try {
+ const { title } = req.params;
+
+ if (!title) {
+ return res.status(400).json({ error: "Missing video title." });
+ }
+
+ const rows = await db.query(
+ "SELECT * FROM class_videos WHERE LOWER(title) LIKE LOWER($1)",
+ [`%${title}%`]
+ );
+
+ if (rows.length === 0) {
+ return res.status(404).json({ error: "No videos found." });
+ }
+
+ res.status(200).json(keysToCamel(keysToCamel(rows)));
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
-export { classVideosRouter };
\ No newline at end of file
+export { classVideosRouter };