From 84b5e14130082183f0c1e4440453ccdd2e4926b3 Mon Sep 17 00:00:00 2001 From: Larry La Date: Mon, 31 Mar 2025 23:19:11 -0700 Subject: [PATCH 1/6] chore: transfer --- backend/projects/models.py | 2 +- backend/projects/views.py | 16 +- frontend/bitmatch/eslint.config.js | 1 + .../bitmatch/src/views/AddProjectPage.jsx | 391 +++++++++++ .../src/views/IndividualProjectPage.jsx | 637 +++++++++++++++--- .../bitmatch/src/views/ProjectListPage.jsx | 19 +- 6 files changed, 955 insertions(+), 111 deletions(-) create mode 100644 frontend/bitmatch/src/views/AddProjectPage.jsx diff --git a/backend/projects/models.py b/backend/projects/models.py index 4a3bd66..eedfb55 100644 --- a/backend/projects/models.py +++ b/backend/projects/models.py @@ -5,7 +5,7 @@ # PROJECTS MODEL class Project(models.Model): # Auto generated - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + id=models.CharField(primary_key=True,default=uuid.uuid4, editable=False, max_length=36) # REQUIRED title = models.CharField(max_length=255, blank=False, null=False) diff --git a/backend/projects/views.py b/backend/projects/views.py index 73bf3c6..b21bb57 100644 --- a/backend/projects/views.py +++ b/backend/projects/views.py @@ -27,7 +27,13 @@ def post(self, request): if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + print(serializer.errors) + error_details = { + "message": "Invalid project data provided.", + "errors": serializer.errors, + } + return Response(error_details, status=status.HTTP_400_BAD_REQUEST) # Get a single project by ID (GET) def get(self, request, pk): @@ -42,7 +48,13 @@ def put(self, request, pk): if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + else: + print(serializer.errors) + error_details = { + "message": "Invalid project data provided.", + "errors": serializer.errors, + } + return Response(error_details, status=status.HTTP_400_BAD_REQUEST) # Delete a project by ID (DELETE) def delete(self, request, pk): diff --git a/frontend/bitmatch/eslint.config.js b/frontend/bitmatch/eslint.config.js index b0f220b..1b66b69 100644 --- a/frontend/bitmatch/eslint.config.js +++ b/frontend/bitmatch/eslint.config.js @@ -34,6 +34,7 @@ export default [ "warn", { allowConstantExport: true }, ], + "react/prop-types": "warn", }, }, ]; diff --git a/frontend/bitmatch/src/views/AddProjectPage.jsx b/frontend/bitmatch/src/views/AddProjectPage.jsx new file mode 100644 index 0000000..7493142 --- /dev/null +++ b/frontend/bitmatch/src/views/AddProjectPage.jsx @@ -0,0 +1,391 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ChevronRight } from "lucide-react"; +import { useNavigate } from "react-router-dom"; + +const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; +const CREATE_PROJECT_ENDPOINT = `${SERVER_HOST}/projects/create/`; + +export default function CreateProjectForm() { + const navigate = useNavigate(); + const [projectName, setProjectName] = useState(""); + const [university, setUniversity] = useState(""); + const [group, setGroup] = useState(""); + const [shortDescription, setShortDescription] = useState(""); + const [fullDescription, setFullDescription] = useState(""); + const [roles, setRoles] = useState([]); + const [newRole, setNewRole] = useState(""); + const [coverImagePreview, setCoverImagePreview] = useState(null); + const [coverImageFile, setCoverImageFile] = useState(null); + const [sliderImages, setSliderImages] = useState([ + "slideshow_img1.jpg", + "slideshow_img2.jpg", + "slideshow_img3.jpg", + "slideshow_img4.jpg", + "slideshow_img5.jpg", + ]); + const [selectedCategories, setSelectedCategories] = useState([ + "Technology", + "Health & Fitness", + ]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const [roleError, setRoleError] = useState(""); + const [successMessage, setSuccessMessage] = useState(""); + + const handleCoverImageUpload = (e) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setCoverImageFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setCoverImagePreview(reader.result); + }; + reader.readAsDataURL(file); + setError(""); + } + }; + + const handleRemoveCoverImage = () => { + setCoverImagePreview(null); + setCoverImageFile(null); + const fileInput = document.getElementById("cover-upload"); + if (fileInput) { + fileInput.value = ""; + } + }; + + const handleShortDescriptionChange = (e) => { + if (e.target.value.length <= 255) { + setShortDescription(e.target.value); + } + }; + + const handleFullDescriptionChange = (e) => { + if (e.target.value.length <= 540) { + setFullDescription(e.target.value); + } + }; + + const handleAddRole = () => { + if (newRole.trim()) { + setRoles((prevRoles) => [...prevRoles, { title: newRole.trim() }]); + setNewRole(""); + setRoleError(""); + } else { + setRoleError("Role can't be empty."); + } + }; + + const handleRemoveRole = (index) => { + setRoles((prevRoles) => prevRoles.filter((_, i) => i !== index)); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(""); + setSuccessMessage(""); + setIsLoading(true); + + if (!projectName.trim()) { + setError("Project Name is required."); + setIsLoading(false); + return; + } + if (!university.trim()) { + setError("University / College Name is required."); + setIsLoading(false); + return; + } + if (!shortDescription.trim()) { + setError("Description is required."); + setIsLoading(false); + return; + } + if (!coverImageFile) { + setError("Cover Image is required."); + setIsLoading(false); + return; + } + + const formData = new FormData(); + formData.append("title", projectName); + formData.append("institution", university); + formData.append("group", group); + formData.append("description", shortDescription); + formData.append("full_description", fullDescription); + formData.append("positions", JSON.stringify(roles)); + formData.append("image_url", coverImageFile); + + try { + const response = await fetch(CREATE_PROJECT_ENDPOINT, { + method: "POST", + body: formData, + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error( + result.message || `HTTP error! status: ${response.status}` + ); + } + + setSuccessMessage("Project created successfully!"); + if (result.id) { + navigate(`/projects/${result.id}`); + } + } catch (err) { + setError( + err.message || "An unexpected error occurred. Please try again." + ); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+ +
+
+
+

+ Add a New Project +

+ +
+
+

+ Cover Image * +

+

+ Minimum: 315x250, Recommended: 630x500 +

+
+ {coverImagePreview ? ( + <> + Cover Preview + + + ) : ( + + )} + +
+
+
+ +
+
+ + setProjectName(e.target.value)} + required + /> +
+ +
+ + setUniversity(e.target.value)} + required + /> +
+ +
+ + setGroup(e.target.value)} + /> +

+ Will this project be associated with a Group? If so, type the + group name. +

+
+ +
+ + +
+ Max Characters: 255 + Character Count: {shortDescription.length} +
+ +

+ + + +
+ Max Characters: 1000 + Character Count: {fullDescription.length} +
+
+ +
+ +
+ { + setNewRole(e.target.value); + setRoleError(""); + }} + className="flex-grow border rounded-md p-2 focus:ring-blue-500 focus:border-blue-500" + placeholder="e.g., Frontend Developer, UI/UX Designer" + /> + +
+ + {roleError && ( +

{roleError}

+ )} + +
+ {roles.length === 0 ? ( +

+ No roles added yet. +

+ ) : ( + roles.map((role, index) => ( +
+ {role.title} + +
+ )) + )} +
+
+ + {error && ( +

+ {error} +

+ )} + {successMessage && ( +

+ {successMessage} +

+ )} +
+ +
+
+
+
+ ); +} diff --git a/frontend/bitmatch/src/views/IndividualProjectPage.jsx b/frontend/bitmatch/src/views/IndividualProjectPage.jsx index 6521536..a06b050 100644 --- a/frontend/bitmatch/src/views/IndividualProjectPage.jsx +++ b/frontend/bitmatch/src/views/IndividualProjectPage.jsx @@ -1,10 +1,21 @@ -import { ChevronRight, ChevronLeft, Plus, Edit, Icon } from "lucide-react"; +import { + ChevronRight, + ChevronLeft, + Plus, + Edit, + Icon, + ThumbsUp, + UserRound, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { MemberCard } from "@/components/project/MemberCard"; +import { PositionCard } from "@/components/project/PositionCard"; import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; -import Modal from "@/components/project/Modal"; +import { DiscussionPost, ReplyForm } from "@/components/project/DiscussionCard"; +import { EditProjectDialog } from "@/components/project/EditProjectDialog"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; import axios from "axios"; @@ -25,7 +36,6 @@ const fetchProjectInfo = async (id) => { } const projectData = await response.json(); - console.log(projectData); return projectData; } catch (error) { @@ -34,14 +44,19 @@ const fetchProjectInfo = async (id) => { } }; +const editProjectInfo = async (id) => {}; + const ProjectDetailPage = () => { const { id } = useParams(); // Access the dynamic `id` parameter from the URL const [project, setProject] = useState(null); // State to store project details const [loading, setLoading] = useState(true); // State to handle loading state const [error, setError] = useState(null); // State to handle errors const [currentImageIndex, setCurrentImageIndex] = useState(0); - const [isModalOpen, setIsModalOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState("overview"); + const [discussions, setDiscussions] = useState([]); + const [showCommentForm, setShowCommentForm] = useState(false); + const [replyingTo, setReplyingTo] = useState(null); useEffect(() => { // Fetch project details when the component mounts or the `id` changes @@ -64,6 +79,42 @@ const ProjectDetailPage = () => { loadProjectInfo(); }, [id]); + const handleSave = async (data) => { + console.log("Saving project data:", data); + + const formData = new FormData(); + + formData.append("title", data.title); + formData.append("group", data.group); + formData.append("institution", data.institution); + formData.append("description", data.description); + formData.append("full_description", data.full_description); + formData.append("positions", JSON.stringify(data.positions)); + + if (data.new_image) { + formData.append("image_url", data.new_image); + } + + try { + const response = await axios.put( + `${SERVER_HOST}/projects/${data.id}/`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + + alert("Project updated successfully!"); + editProjectInfo(response.data); + window.location.reload(); + } catch (error) { + console.error("Error updating project:", error); + alert("Failed to update the project. Please try again."); + } + }; + const nextImage = () => { if (project.images) { setCurrentImageIndex((prev) => @@ -84,6 +135,100 @@ const ProjectDetailPage = () => { setCurrentImageIndex(index); }; + const handleAddComment = () => { + setShowCommentForm(true); + setReplyingTo(null); + }; + + const handleCancelComment = () => { + setShowCommentForm(false); + setReplyingTo(null); + }; + + const handleSubmitComment = (postId, content) => { + if (postId) { + // Add reply to existing post + const updatedDiscussions = discussions.map((discussion) => { + if (discussion.id === postId) { + return { + ...discussion, + replies: [ + ...(discussion.replies || []), + { + id: Date.now().toString(), + parentId: postId, + author: { + name: "Current User", + title: "Project Member", + profileImage: "", + }, + content, + datePosted: new Date().toLocaleString(), + }, + ], + }; + } + return discussion; + }); + setDiscussions(updatedDiscussions); + } else { + // Add new post + const newPost = { + id: Date.now().toString(), + author: { + name: "Current User", + title: "Project Member", + profileImage: "", + }, + content, + datePosted: new Date().toLocaleString(), + replies: [], + }; + setDiscussions([...discussions, newPost]); + } + setShowCommentForm(false); + setReplyingTo(null); + }; + + const handleReplyToPost = (id) => { + setReplyingTo(id); + }; + + const handleDeletePost = (id) => { + // In a real app, this would show a confirmation dialog + if (confirm(`Delete post with ID ${id}?`)) { + // Check if it's a main post or a reply + const isMainPost = discussions.some((discussion) => discussion.id === id); + + if (isMainPost) { + // Delete the main post and all its replies + const updatedDiscussions = discussions.filter( + (discussion) => discussion.id !== id + ); + setDiscussions(updatedDiscussions); + } else { + // Delete a reply + const updatedDiscussions = discussions.map((discussion) => { + if ( + discussion.replies && + discussion.replies.some((reply) => reply.id === id) + ) { + return { + ...discussion, + replies: discussion.replies.filter((reply) => reply.id !== id), + }; + } + return discussion; + }); + setDiscussions(updatedDiscussions); + } + } + }; + + const handleReaction = (id) => { + alert(`Add reaction to post with ID ${id}`); + }; + // Loading state if (loading) { return ( @@ -119,19 +264,19 @@ const ProjectDetailPage = () => { onClick={() => window.history.back()} > - Back + Back to Projects {/* Main content */}
-

{project.title}

+

{project.title}

{/* Project showcase */} -
+
{/* Image Slider */} -
+
{project.images && project.images.length > 0 ? ( <>
@@ -142,7 +287,7 @@ const ProjectDetailPage = () => { alt="Project image" className="object-cover" /> -
+
-
-

- Slider -
- Image snapshots are below -

-
+
) : ( No images available @@ -175,7 +314,7 @@ const ProjectDetailPage = () => {
{/* Project info */}
-
+
{project.image_url ? ( { Cover Image goes here )}
-
+
From

{project.group}

{project.description}

-
+ {project.likes_count} Likes
-
+ {project.followers_count} Followers
@@ -206,7 +349,7 @@ const ProjectDetailPage = () => {
{/* Thumbnails */} -
+
{project.images && project.images.map((image, index) => ( @@ -220,8 +363,8 @@ const ProjectDetailPage = () => { {`Pic
@@ -231,149 +374,451 @@ const ProjectDetailPage = () => { {/* Tabs */} { + const tabNames = [ + "overview", + "updates", + "members", + "wanted", + "discussions", + "contact", + "edit", + ]; + const selectedTab = tabNames[index]; + + if (selectedTab === "edit") { + setIsOpen(true); // Open the dialog + setActiveTab("overview"); // Stay on the default tab (e.g., Overview) + } else { + setActiveTab(selectedTab); // Change tab normally + } + }} > - + Overview Updates Members Wanted Discussions Contact + + { + setIsOpen(true); + e.preventDefault(); + }} + > + Edit Project + + +
-

Overview

+

Overview

-

+

Background & More Details About the Project -

+
-
- This space will be filled in by the owner -
-

{project.description}

- - setIsModalOpen(false)} - > -

Edit Project

-
-

Title

- -

Description

- -
- -
+

{project.full_description}

-
-

- Updates content will go here -

+
+
+

+ Updates{" "} + (W.I.P) +

+
+

Title

+ + {showCommentForm ? ( + + ) : ( +
+
+
+

+ Add your comments here +

+
+
+
+ + +
+
+ )} +
+
-
-

- Members content will go here -

+
+ {/* Search and Add Member Section */} +
+
+ + +
+
+ + {/* Members List Section */} +
+

+ Students Working on This Project{" "} + (W.I.P) +

+ +
+ {/* Member 1 */} + + {/* Member 2 */} + + {/* Member 3 */} + +
+
-
-

- Wanted content will go here -

+
+ {/* Wanted Header */} +

+ Wanted{" "} + (W.I.P) +

+ {/* Positions Section */} +
+
+

+ Positions Needed for This Project +

+ +
+ +
+ + +
+
- -
-

- Discussions content will go here -

+ +
+ {/* Discussions Header */} +
+
+ + Discussions{" "} + (W.I.P) + +
+
+ + {/* Comment Form */} +
+ {showCommentForm ? ( + + ) : ( +
+
+
+

+ Add your comments here +

+
+
+
+ + +
+
+ )} + + {/* Discussion Posts */} +
+ {discussions.length > 0 ? ( + discussions.map((discussion) => ( +
+ + + {replyingTo === discussion.id && ( +
+ +
+ )} + + {discussion.replies && + discussion.replies.map((reply) => ( +
+ + + {replyingTo === reply.id && ( +
+ +
+ )} +
+ ))} +
+ )) + ) : ( +

+ No discussions yet. Start the conversation! +

+ )} +
+
-
-

- Contact content will go here -

+
+
+

+ Contact The Owner of This Project{" "} + (W.I.P) +

+

Full Name

+ +

Email Address

+ +

Subject

+ +

Description

+ + +
+
{/* Footer */}
-

Footer

+

diff --git a/frontend/bitmatch/src/views/ProjectListPage.jsx b/frontend/bitmatch/src/views/ProjectListPage.jsx index da0099f..97aa371 100644 --- a/frontend/bitmatch/src/views/ProjectListPage.jsx +++ b/frontend/bitmatch/src/views/ProjectListPage.jsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { ChevronRight, Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Link } from "react-router-dom"; import ProjectCardLarge from "@/components/project/ProjectCardLarge"; const SERVER_HOST = import.meta.env.VITE_SERVER_HOST; import axios from "axios"; @@ -51,21 +52,15 @@ export default function ProjectListPage() {

Public Projects List

- + + +
- {/* "More" Button */} -
- -
- {/* Project Grid */}
{projects.map((project, index) => ( From bb0ad09b407f24bcb8b301a8392f14c006e8d664 Mon Sep 17 00:00:00 2001 From: Larry La Date: Mon, 31 Mar 2025 23:22:12 -0700 Subject: [PATCH 2/6] chore: adding more files --- .../src/components/project/DiscussionCard.jsx | 125 +++++++ .../components/project/EditProjectDialog.jsx | 311 ++++++++++++++++++ .../src/components/project/MemberCard.jsx | 63 ++++ .../src/components/project/PositionCard.jsx | 119 +++++++ frontend/bitmatch/src/components/ui/Card.jsx | 60 ++++ .../bitmatch/src/components/ui/Dialog.jsx | 101 ++++++ .../src/components/ui/ImageSlideshow.jsx | 10 +- frontend/bitmatch/src/components/ui/Label.jsx | 21 ++ .../src/components/ui/ProjectCarousel.jsx | 10 +- .../bitmatch/src/components/ui/ScrollArea.jsx | 43 +++ .../bitmatch/src/components/ui/Textarea.jsx | 18 + 11 files changed, 870 insertions(+), 11 deletions(-) create mode 100644 frontend/bitmatch/src/components/project/DiscussionCard.jsx create mode 100644 frontend/bitmatch/src/components/project/EditProjectDialog.jsx create mode 100644 frontend/bitmatch/src/components/project/MemberCard.jsx create mode 100644 frontend/bitmatch/src/components/project/PositionCard.jsx create mode 100644 frontend/bitmatch/src/components/ui/Card.jsx create mode 100644 frontend/bitmatch/src/components/ui/Dialog.jsx create mode 100644 frontend/bitmatch/src/components/ui/Label.jsx create mode 100644 frontend/bitmatch/src/components/ui/ScrollArea.jsx create mode 100644 frontend/bitmatch/src/components/ui/Textarea.jsx diff --git a/frontend/bitmatch/src/components/project/DiscussionCard.jsx b/frontend/bitmatch/src/components/project/DiscussionCard.jsx new file mode 100644 index 0000000..a3e2bd4 --- /dev/null +++ b/frontend/bitmatch/src/components/project/DiscussionCard.jsx @@ -0,0 +1,125 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; + +export function ReplyForm({ postId, onCancel, onSubmit }) { + const [content, setContent] = useState(""); + + const handleSubmit = () => { + if (content.trim()) { + onSubmit(postId, content); + setContent(""); + } + }; + + return ( +
+
+