From eb7fdc86c77475bebcbd647f2f5b599d8b7e81b1 Mon Sep 17 00:00:00 2001
From: Kapil jangid <103230903+CuriousCoder00@users.noreply.github.com>
Date: Mon, 21 Oct 2024 20:03:53 +0530
Subject: [PATCH] Feat/project stack (#527)
* Refactor User model in schema.prisma
* Refactor user.profile.validator.ts: Add stack enum for projectSchema
* Refactor user-multistep-form: Add project stack selection
* Refactor project schema to add default value for stack field
* Refactor schema.prisma: Add cascade deletion for user relations
* Refactor project schema: Add projectThumbnail field and update projectSummary
* Refactor user.profile.validator.ts: Add optional projectThumbnail field to projectSchema
* Refactor profile pages: Add container and heading
* Refactor AddProject form: Add project thumbnail upload functionality
* Refactor UserProject component: Add project thumbnail display and link functionality
* Refactor project schema: Update ProjectStack enum values
* Refactor project schema: Update ProjectStack enum values
* Refactor icons.ts: Add loading spinner icon
* Refactor profile pages: Add "Add more" functionality
* Refactor project schema: Update ProjectStack enum values
* Refactor UserProject component: Add loading spinner and no projects found message
* Refactor UserExperience component: Improve loading and no experiences found handling
---
prisma/schema.prisma | 74 ++++----
src/app/profile/experience/page.tsx | 24 ++-
src/app/profile/projects/page.tsx | 24 ++-
src/app/profile/resume/page.tsx | 5 +-
src/app/profile/skills/page.tsx | 24 ++-
src/components/profile/UserExperience.tsx | 78 ++++----
src/components/profile/UserProject.tsx | 66 +++++--
.../user-multistep-form/add-project-form.tsx | 175 +++++++++++++++++-
src/lib/icons.ts | 2 +
src/lib/validators/user.profile.validator.ts | 10 +
10 files changed, 391 insertions(+), 91 deletions(-)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 51b13f7a..a1082e84 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -8,8 +8,8 @@ datasource db {
}
model User {
- id String @id @default(cuid())
- name String
+ id String @id @default(cuid())
+ name String
password String?
avatar String?
@@ -19,34 +19,34 @@ model User {
email String @unique
emailVerified DateTime?
-
- skills String[]
- experience Experience[]
- project Project[]
- resume String?
-
- oauthProvider OauthProvider? // Tracks OAuth provider (e.g., 'google')
- oauthId String?
+
+ skills String[]
+ experience Experience[]
+ project Project[]
+ resume String?
+
+ oauthProvider OauthProvider? // Tracks OAuth provider (e.g., 'google')
+ oauthId String?
blockedByAdmin DateTime?
- onBoard Boolean @default(false)
+ onBoard Boolean @default(false)
}
enum OauthProvider {
GOOGLE
}
-
model VerificationToken {
- token String
+ token String
identifier String
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- type TokenType
- @@unique([token,identifier])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ type TokenType
+
+ @@unique([token, identifier])
}
-enum TokenType {
+enum TokenType {
EMAIL_VERIFICATION
RESET_PASSWORD
}
@@ -80,12 +80,12 @@ model Job {
isVerifiedJob Boolean @default(false) @map("is_verified_job")
postedAt DateTime @default(now())
updatedAt DateTime @updatedAt
- user User @relation(fields: [userId], references: [id],onDelete: Cascade)
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Experience {
- id Int @id @default(autoincrement())
- companyName String
+ id Int @id @default(autoincrement())
+ companyName String
designation String
EmploymentType EmployementType
address String
@@ -93,19 +93,31 @@ model Experience {
currentWorkStatus Boolean
startDate DateTime
endDate DateTime?
- description String
- userId String
- user User @relation(fields: [userId] ,references: [id])
+ description String
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Project {
- id Int @id @default(autoincrement())
- projectName String
- projectSummary String
- projectLiveLink String?
- projectGithub String
- userId String
- user User @relation(fields: [userId] , references: [id])
+ id Int @id @default(autoincrement())
+ projectName String
+ projectThumbnail String?
+ projectSummary String
+ projectLiveLink String?
+ projectGithub String
+ stack ProjectStack @default(OTHERS)
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+enum ProjectStack {
+ GO
+ PYTHON
+ MERN
+ NEXTJS
+ AI_GPT_APIS
+ SPRINGBOOT
+ OTHERS
}
enum Currency {
diff --git a/src/app/profile/experience/page.tsx b/src/app/profile/experience/page.tsx
index adf6b8b0..e657a7a9 100644
--- a/src/app/profile/experience/page.tsx
+++ b/src/app/profile/experience/page.tsx
@@ -1,5 +1,13 @@
'use client';
import { UserExperience } from '@/components/profile/UserExperience';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { AddExperience } from '@/components/user-multistep-form/addExperience-form';
import APP_PATHS from '@/config/path.config';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
@@ -13,7 +21,21 @@ export default function AccountExperiencePage() {
router.push(`${APP_PATHS.SIGNIN}?redirectTo=/profile`);
}, [session.status, router]);
return (
-
+
+
+
Experience
+
+
);
diff --git a/src/app/profile/projects/page.tsx b/src/app/profile/projects/page.tsx
index e92c1775..517a3480 100644
--- a/src/app/profile/projects/page.tsx
+++ b/src/app/profile/projects/page.tsx
@@ -1,5 +1,13 @@
'use client';
import { UserProjects } from '@/components/profile/UserProject';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { AddProject } from '@/components/user-multistep-form/add-project-form';
import APP_PATHS from '@/config/path.config';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
@@ -13,7 +21,21 @@ export default function AccountProjectPage() {
router.push(`${APP_PATHS.SIGNIN}?redirectTo=/profile`);
}, [session.status, router]);
return (
-
+
+
+
Projects
+
+
);
diff --git a/src/app/profile/resume/page.tsx b/src/app/profile/resume/page.tsx
index b0639fa9..f9efcefe 100644
--- a/src/app/profile/resume/page.tsx
+++ b/src/app/profile/resume/page.tsx
@@ -13,7 +13,10 @@ export default function AccountResumePage() {
router.push(`${APP_PATHS.SIGNIN}?redirectTo=/profile`);
}, [session.status, router]);
return (
-
+
);
diff --git a/src/app/profile/skills/page.tsx b/src/app/profile/skills/page.tsx
index 8a731a40..c9fd1b2b 100644
--- a/src/app/profile/skills/page.tsx
+++ b/src/app/profile/skills/page.tsx
@@ -1,5 +1,13 @@
'use client';
import { UserSkills } from '@/components/profile/UserSkills';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { AddSkills } from '@/components/user-multistep-form/add-skills-form';
import APP_PATHS from '@/config/path.config';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
@@ -13,7 +21,21 @@ export default function AccountResumePage() {
router.push(`${APP_PATHS.SIGNIN}?redirectTo=/profile`);
}, [session.status, router]);
return (
-
+
+
+
Skills
+
+
);
diff --git a/src/components/profile/UserExperience.tsx b/src/components/profile/UserExperience.tsx
index 366ef085..16438d4f 100644
--- a/src/components/profile/UserExperience.tsx
+++ b/src/components/profile/UserExperience.tsx
@@ -2,9 +2,8 @@ import { getUserExperience } from '@/actions/user.profile.actions';
import { useEffect, useState } from 'react';
import { useToast } from '../ui/use-toast';
import { Experience } from '@prisma/client';
-import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import _ from 'lodash';
-
+import icons from '@/lib/icons';
export function UserExperience() {
const { toast } = useToast();
const [experiences, setExperiences] = useState
();
@@ -34,53 +33,52 @@ export function UserExperience() {
}, []);
if (!experiences) {
- return null;
+ return (
+
+
+
+ );
}
return (
{experiences.map((item: Experience) => (
-
-
-
- Company Name:
- {item.companyName}
-
-
-
-
- Designation: {item.designation}
-
-
- Employment Type:{' '}
- {_.startCase(item.EmploymentType)}
-
-
- Work Mode: {item.workMode}
-
-
- Current Status:{' '}
- {item.currentWorkStatus
- ? 'Currently Employed here'
- : 'Not Currently Employed here'}
-
-
- Duration:{' '}
- {new Date(item.startDate).toLocaleDateString()}{' '}
- {item.endDate
- ? ` - ${new Date(item.endDate).toLocaleDateString()}`
- : ' - Present'}
-
-
- Description:
+
+
+
+ {new Date(item.startDate).toLocaleDateString()}
+ {item.endDate
+ ? ` - ${new Date(item.endDate).toLocaleDateString()}`
+ : ' - Present'}
+
+
+ {_.startCase(item.EmploymentType)}, {_.startCase(item.workMode)}
+
+
+
+
{item.companyName}
+
+ {item.designation}
+
+
+
+
+
{item.description}
-
-
-
+
+
+
))}
+ {experiences.length === 0 && (
+
+
+ No Experiences Found
+
+ )}
);
}
diff --git a/src/components/profile/UserProject.tsx b/src/components/profile/UserProject.tsx
index 5da4af54..b8ab2d9e 100644
--- a/src/components/profile/UserProject.tsx
+++ b/src/components/profile/UserProject.tsx
@@ -2,6 +2,7 @@ import { getUserProjects } from '@/actions/user.profile.actions';
import { useEffect, useState } from 'react';
import { useToast } from '../ui/use-toast';
import { Project } from '@prisma/client';
+import icons from '@/lib/icons';
import Link from 'next/link';
import {
Card,
@@ -10,10 +11,12 @@ import {
CardHeader,
CardTitle,
} from '../ui/card';
+import { SquareArrowOutUpRightIcon } from 'lucide-react';
export function UserProjects() {
const { toast } = useToast();
const [projects, setProjects] = useState
();
+
useEffect(() => {
async function fetchProjects() {
try {
@@ -39,42 +42,75 @@ export function UserProjects() {
}, [toast]);
if (!projects) {
- return null;
+ return (
+
+
+
+ );
}
return (
-
+
{projects.map((item: Project) => (
+
+ {item.projectThumbnail ? (
+
+ ) : (
+
+
+ {item.projectName}
+
+
+ )}
+
-
+
{item.projectName}
-
+
{item.projectSummary}
- {item.projectLiveLink && (
+ {item.stack && (
+
+ Stack:
+ {item.stack}
+
+ )}
+
+ {item.projectLiveLink && (
+
+
+
+ )}
- Live Project
+
- )}
-
- GitHub Repository
-
+
))}
+ {projects.length === 0 && (
+
+
+ No Projects Found
+
+ )}
);
}
diff --git a/src/components/user-multistep-form/add-project-form.tsx b/src/components/user-multistep-form/add-project-form.tsx
index 7c16e967..3a555e60 100644
--- a/src/components/user-multistep-form/add-project-form.tsx
+++ b/src/components/user-multistep-form/add-project-form.tsx
@@ -17,25 +17,108 @@ import { Button } from '../ui/button';
import { Textarea } from '../ui/textarea';
import { useToast } from '../ui/use-toast';
import { addUserProjects } from '@/actions/user.profile.actions';
-import { useState } from 'react';
+import { useRef, useState } from 'react';
import { LoadingSpinner } from '../loading-spinner';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '../ui/select';
+import { uploadFileAction } from '@/actions/upload-to-cdn';
+import { FaFileUpload } from 'react-icons/fa';
+import Image from 'next/image';
+import { X } from 'lucide-react';
export const AddProject = () => {
const form = useForm
({
resolver: zodResolver(projectSchema),
defaultValues: {
+ projectThumbnail: '',
projectName: '',
projectSummary: '',
projectGithub: '',
projectLiveLink: '',
+ stack: 'OTHERS',
},
});
const [isLoading, setIsLoading] = useState(false);
const { toast } = useToast();
+ const [file, setFile] = useState(null);
+ const [previewImg, setPreviewImg] = useState(null);
+
+ const projectThumbnail = useRef(null);
+
+ const handleClick = () => {
+ const fileInput = document.getElementById('fileInput') as HTMLInputElement;
+
+ if (fileInput) {
+ fileInput.click();
+ }
+ };
+ const clearImage = () => {
+ const fileInput = document.getElementById('fileInput') as HTMLInputElement;
+
+ if (fileInput) {
+ fileInput.value = '';
+ }
+ setPreviewImg(null);
+ setFile(null);
+ };
+
+ const submitImage = async (file: File | null) => {
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const uniqueFileName = `${Date.now()}-${file.name}`;
+ formData.append('uniqueFileName', uniqueFileName);
+
+ const res = await uploadFileAction(formData, 'webp');
+ if (!res) {
+ throw new Error('Failed to upload image');
+ }
+
+ const uploadRes = res;
+ return uploadRes.url;
+ } catch (error) {
+ console.error('Image upload failed:', error);
+ }
+ };
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const selectedFile = e.target.files ? e.target.files[0] : null;
+ if (!selectedFile) {
+ return;
+ }
+ if (!selectedFile.type.includes('image')) {
+ toast({
+ title:
+ 'Invalid file format. Please upload an image file (e.g., .png, .jpg, .jpeg, .svg ) for the company logo',
+ variant: 'destructive',
+ });
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = () => {
+ if (projectThumbnail.current) {
+ projectThumbnail.current.src = reader.result as string;
+ }
+ setPreviewImg(reader.result as string);
+ };
+ reader.readAsDataURL(selectedFile);
+ if (selectedFile) {
+ setFile(selectedFile);
+ }
+ };
const onSubmit = async (data: projectSchemaType) => {
try {
setIsLoading(true);
+ data.projectThumbnail = (await submitImage(file)) ?? '';
const response = await addUserProjects(data);
if (!response.status) {
return toast({
@@ -47,6 +130,7 @@ export const AddProject = () => {
title: response.message,
variant: 'success',
});
+ setPreviewImg(null);
form.reset(form.formState.defaultValues);
} catch (_error) {
toast({
@@ -62,6 +146,53 @@ export const AddProject = () => {
return (