Skip to content

Commit

Permalink
Merge pull request #68 from deepraj21/main
Browse files Browse the repository at this point in the history
Add image banner support to project
  • Loading branch information
deepraj21 authored Oct 24, 2024
2 parents 9cbb5f9 + da11e5e commit e6a2748
Show file tree
Hide file tree
Showing 15 changed files with 592 additions and 160 deletions.
4 changes: 4 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import EditProfileForm from './pages/EditProfileForm';
import { MessagePage } from './pages/MessagePage';
import Projects from './pages/Projects';
import Visualization from './pages/Visualization';
import PrivacyPolicy from './pages/Privacypolicy';
import ProjectDisplay from './pages/ProjectDisplay';

const App = () => {

Expand All @@ -24,8 +26,10 @@ const App = () => {
<Route path="/settings" element={<EditProfileForm />} />
<Route path="/message" element={<MessagePage/>} />
<Route path="/projects/:username" element={<Projects />} />
<Route path="/projects/:username/:projectId" element={<ProjectDisplay />} />
<Route path="/user/:username" element={<Profile />} />
<Route path="/relations/:username" element={<Visualization />} />
<Route path="/privacy-policy" element={<PrivacyPolicy/>} />
<Route path="*" element={<div>404</div>} />
</Routes>
</Router>
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ const Footer = () => {
<ul className="text-gray-500 dark:text-gray-400 font-medium">
<li className="mb-4">
<a href="/" className="hover:underline">
Devhub
Posts
</a>
</li>
<li>
<a href="/" className="hover:underline">
Tailwind CSS
DevMap
</a>
</li>
</ul>
Expand Down Expand Up @@ -63,7 +63,7 @@ const Footer = () => {
</h2>
<ul className="text-gray-500 dark:text-gray-400 font-medium">
<li className="mb-4">
<a href="#" className="hover:underline">
<a href="/privacy-policy" className="hover:underline">
Privacy Policy
</a>
</li>
Expand Down
158 changes: 92 additions & 66 deletions client/src/components/Projects/AddProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Cross1Icon } from "@radix-ui/react-icons";
Expand All @@ -16,6 +15,7 @@ import { toast } from 'sonner';
import { Button } from "@/components/ui/button";
import { Icons } from "@/components/ui/icons";
import { Textarea } from '../ui/textarea';
import UploadComponent from './UploadComponent';

const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';

Expand All @@ -24,6 +24,7 @@ interface Project {
description: string;
repoLink: string;
tags: Tag[];
imageUrl?: string;
}

interface Tag {
Expand All @@ -37,8 +38,10 @@ const AddProject: React.FC<{ onProjectChange: () => void }> = ({ onProjectChange
description: '',
repoLink: '',
tags: [],
imageUrl: '', // Add image URL state
});

const [imageFile, setImageFile] = useState<File | null>(null); // State for handling image file
const username = localStorage.getItem('devhub_username');
const [isLoading, setIsLoading] = useState(false);

Expand All @@ -50,16 +53,39 @@ const AddProject: React.FC<{ onProjectChange: () => void }> = ({ onProjectChange
}));
};

// Function to upload image to Cloudinary via the backend
const uploadImage = async () => {
if (!imageFile) return ''; // Return empty string if no image is uploaded

const formData = new FormData();
formData.append('image', imageFile);

try {
const response = await axios.post(`${backendUrl}/project/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});

return response.data.imageUrl; // Return the image URL
} catch (error) {
console.error('Image upload failed:', error);
toast.error('Failed to upload image');
return '';
}
};

const handleAddProject = async () => {
if (!username) {
toast.error('Username not found');
return;
}

setIsLoading(true);
setIsLoading(true);

try {
// Convert tags array to a comma-separated string
const imageUrl = await uploadImage(); // Upload the image and get the URL

const tagsString = newProject.tags.map((tag) => tag.value).join(',');

const response = await axios.post(
Expand All @@ -68,14 +94,16 @@ const AddProject: React.FC<{ onProjectChange: () => void }> = ({ onProjectChange
title: newProject.title,
description: newProject.description,
repo_link: newProject.repoLink,
tags: tagsString, // Send as comma-separated string
tags: tagsString, // Send as comma-separated string
imageUrl, // Include the uploaded image URL
},
{ withCredentials: true },
);

if (response.status === 200 || response.status === 201) {
toast.success('Project added successfully');
setNewProject({ title: '', description: '', repoLink: '', tags: [] }); // Reset form
setNewProject({ title: '', description: '', repoLink: '', tags: [], imageUrl: '' }); // Reset form
setImageFile(null); // Reset the image file
onProjectChange();
} else {
toast.error('Failed to add project');
Expand All @@ -84,80 +112,78 @@ const AddProject: React.FC<{ onProjectChange: () => void }> = ({ onProjectChange
console.error('Failed to add project:', error);
toast.error('An error occurred while adding the project');
} finally {
setIsLoading(false); // Reset loading state
setIsLoading(false); // Reset loading state
}
};




return (
<AlertDialog>
<AlertDialogTrigger className="w-[120px] ml-5 mt-5 mb-3">
<Button variant="outline" className="h-[50px]">Add Project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<div className='flex'>
<AlertDialogHeader className='text-2xl'>Add New Project</AlertDialogHeader>
<AlertDialogHeader className='text-2xl mt-1.5'>Add New Project</AlertDialogHeader>
<div className='flex-grow'></div>
<AlertDialogCancel><Cross1Icon className='h-3 w-3'/></AlertDialogCancel>
<AlertDialogCancel><Cross1Icon className='h-3 w-3' /></AlertDialogCancel>
</div>
<AlertDialogDescription>
<AlertDialogTitle></AlertDialogTitle>
<div className="grid gap-6 sm:w-80">
<div>
<Input
id="newProjectTitle"
name="title"
value={newProject.title}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="Project title"
className="mt-2"
/>
</div>
<div>
<Textarea
id="newProjectDescription"
name="description"
value={newProject.description}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="# Project description"
className=""
/>
</div>
<div>
<TagInput
selectedTags={newProject.tags}
onTagsChange={(tags) => setNewProject({ ...newProject, tags })}
/>
</div>
<div>

<Input
id="newProjectRepoLink"
name="repoLink"
value={newProject.repoLink}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="Repository link"
className=""
/>
</div>
<Button onClick={handleAddProject} disabled={isLoading} className="w-full mt-4">
{isLoading ? (
<>
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
Adding...
</>
) : (
'Add Project'
)}
</Button>
</div>
<AlertDialogDescription>
<div className="grid gap-6 sm:w-80">
<div>
<UploadComponent onFileChange={setImageFile} />
</div>
<div>
<Input
id="newProjectTitle"
name="title"
value={newProject.title}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="Project title"
className="mt-2"
/>
</div>
<div>
<Textarea
id="newProjectDescription"
name="description"
value={newProject.description}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="# Project description"
className=""
/>
</div>
<div>
<TagInput
selectedTags={newProject.tags}
onTagsChange={(tags) => setNewProject({ ...newProject, tags })}
/>
</div>
<div>
<Input
id="newProjectRepoLink"
name="repoLink"
value={newProject.repoLink}
onChange={handleProjectChange}
disabled={isLoading}
placeholder="Repository link"
className=""
/>
</div>

</AlertDialogDescription>
<Button onClick={handleAddProject} disabled={isLoading} className="w-full mt-4">
{isLoading ? (
<>
<Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
Adding...
</>
) : (
'Add Project'
)}
</Button>
</div>
</AlertDialogDescription>
</AlertDialogContent>
</AlertDialog>
);
Expand Down
32 changes: 28 additions & 4 deletions client/src/components/Projects/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { StarIcon, GitHubLogoIcon, TrashIcon, Pencil2Icon } from "@radix-ui/react-icons";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { FaStar as FilledStarIcon } from "react-icons/fa";
import {
AlertDialog,
Expand All @@ -14,13 +14,17 @@ import {
import UpdateProject from "./UpdateProject";
import DeleteProject from "./DeleteProject";
import { Cross1Icon } from "@radix-ui/react-icons";
import { Skeleton } from "../ui/skeleton";
import { useNavigate } from "react-router-dom";
import ReactMarkdown from 'react-markdown';

interface Project {
projectId: string;
title: string;
description: string;
repoLink: string;
starCount: number;
imageUrl: string;
tags: string[];
}

Expand All @@ -34,7 +38,13 @@ const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
export function ProjectCard({ project, onProjectChange }: ProjectProps) {
const [isStarred, setIsStarred] = useState(false);
const [starCount, setStarCount] = useState(project.starCount);
const username = localStorage.getItem('devhub_username');
const username = localStorage.getItem('devhub_username');

const navigate = useNavigate();

const projectDetails = () => {
navigate(`/projects/${username}/${project.projectId}`);
}

// Fetch initial star state from server/localStorage or logic to check if user has starred
useEffect(() => {
Expand Down Expand Up @@ -68,10 +78,24 @@ export function ProjectCard({ project, onProjectChange }: ProjectProps) {

return (
<Card>
{
project.imageUrl ? (
<div className="h-40 w-full bg-primary rounded-tl-md rounded-tr-md" style={{backgroundImage: `url(${project.imageUrl})`}}></div>
):
(
<Skeleton className="h-40 w-full rounded-tl-md rounded-tr-md" />
)
}
<CardHeader className="grid grid-cols-[1fr_110px] items-start gap-4 space-y-0">

<div className="space-y-1">
<CardTitle>{project.title}</CardTitle>
<CardDescription>{project.description}</CardDescription>
<CardTitle
onClick={projectDetails}
className="hover:underline cursor-pointer"
>
{project.title}
</CardTitle>
<ReactMarkdown>{project.description}</ReactMarkdown>
</div>
<div className="flex items-center rounded-md bg-secondary text-secondary-foreground">
<Button
Expand Down
Loading

0 comments on commit e6a2748

Please sign in to comment.