(
+
+
+ {children}
+
+
+
+
+));
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
+
+const ScrollBar = React.forwardRef(
+ ({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+ )
+);
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
+
+export { ScrollArea, ScrollBar };
\ No newline at end of file
diff --git a/frontend/bitmatch/src/components/ui/Textarea.jsx b/frontend/bitmatch/src/components/ui/Textarea.jsx
new file mode 100644
index 0000000..d362b5e
--- /dev/null
+++ b/frontend/bitmatch/src/components/ui/Textarea.jsx
@@ -0,0 +1,18 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+const Textarea = React.forwardRef(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+Textarea.displayName = "Textarea";
+
+export { Textarea };
\ No newline at end of file
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 (
+
+ );
+}
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 */}