diff --git a/src/actions/chat_actions.ts b/src/actions/chat_actions.ts index 18567f0..fddb148 100644 --- a/src/actions/chat_actions.ts +++ b/src/actions/chat_actions.ts @@ -6,7 +6,6 @@ interface JobsResponse { } export const fetchJobs = async (chatdata: any) => { - console.log("Sending API request with data:", chatdata); const token = await getCookie("token"); try { @@ -35,4 +34,93 @@ export const fetchJobs = async (chatdata: any) => { console.error("Error fetching jobs:", error) throw error; // Re-throw to handle in the component } +} + +export const deleteById = async (path: string) => { + const token = await getCookie("token"); + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}${path}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Cookie: `token=${token}`, + }, + credentials: "include", + } + ); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error deleting resource:", error); + throw error; + } +} + +export const renameChatById = async (chatId: string, newName: string) => { + const token = await getCookie("token"); + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/chat/rename?id=${chatId}`, + { + method: "PUT", +headers: { +"Content-Type": "application/json", +}, + + credentials: "include", + body: JSON.stringify({ + name: newName, + }), + } + ); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + console.log("Chat renamed successfully:", data); + return data; + } catch (error) { + console.error("Error renaming chat:", error); + throw error; + } +} + +export const fetchChats = async () => { + const token = await getCookie("token"); + + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/chat/all`, + { + method: "GET", +headers: { +"Content-Type": "application/json", +}, + + credentials: "include", + } + ); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + console.log("Chats fetched successfully:", data); + return data; + } catch (error) { + console.error("Error fetching chats:", error); + throw error; + } } \ No newline at end of file diff --git a/src/app/(root)/(chat)/_components/heroSection.tsx b/src/app/(root)/(chat)/_components/heroSection.tsx index e8b938a..77db9c7 100644 --- a/src/app/(root)/(chat)/_components/heroSection.tsx +++ b/src/app/(root)/(chat)/_components/heroSection.tsx @@ -3,8 +3,7 @@ import Image from 'next/image'; import { FaPaperclip, FaUpload, FaGlobe, FaTwitter, FaLinkedin, FaGithub } from 'react-icons/fa'; import { useGoogleLogin } from '@react-oauth/google'; import { useAppDispatch } from '@/redux/hooks'; -import axios from 'axios'; -import { userData } from '@/redux/slices/userSlice'; +import GlowButton from '@/components/ui/glowButton'; import toast from 'react-hot-toast'; import { Loader2, SparklesIcon } from 'lucide-react'; import JobSearchResults from './job-result'; @@ -28,6 +27,13 @@ const HeroSection = ({ isLoggedIn }: { isLoggedIn: boolean }) => { const [showResumePopup, setShowResumePopup] = useState(false); // New state for resume popup const [jobApiData, setJobApiData] = useState(null) const [isApiLoading, setIsApiLoading] = useState(false) + const buttonItems = [ + { text: 'NEW', secondaryText: 'Build a mobile app with Expo', icon: null }, + { text: 'Recharts dashboard', secondaryText: null, icon: null }, + { text: 'Habit tracker', secondaryText: null, icon: null }, + { text: 'Real estate listings', secondaryText: null, icon: null }, + { text: 'Developer portfolio', secondaryText: null, icon: null } + ]; useEffect(() => { const storedCount = localStorage.getItem(STORAGE_KEY); @@ -98,7 +104,7 @@ const HeroSection = ({ isLoggedIn }: { isLoggedIn: boolean }) => { transition: 'opacity 0.25s linear', } as React.CSSProperties} > - {/* Light Rays (existing code) */} + {/* Light Ray 1 */}
{ filter: 'blur(110px)' }} /> - {/* Other light rays... */} + {/* Light Ray 2 */} +
+ {/* Light Ray 3 */} +
+ {/* Light Ray 4 */} +
+ {/* Light Ray 5 */} +
{/* Conditional Rendering for Hero Content or Job Search Results */} @@ -173,15 +236,16 @@ const HeroSection = ({ isLoggedIn }: { isLoggedIn: boolean }) => { {/* Only show suggestion buttons when no results */} {(!showResults || queryCount >= QUERY_LIMIT) && (
- {['Recharts dashboard', 'Habit tracker', 'Real estate listings', 'Developer portfolio'].map((item) => ( - - ))} -
+ {buttonItems.slice(1).map((item) => ( + + {item.text} + + ))} +
)}
)} diff --git a/src/app/(root)/(chat)/_components/sidebar.tsx b/src/app/(root)/(chat)/_components/sidebar.tsx index f2d89aa..3c79acb 100644 --- a/src/app/(root)/(chat)/_components/sidebar.tsx +++ b/src/app/(root)/(chat)/_components/sidebar.tsx @@ -1,12 +1,99 @@ -import React from 'react' -import { Folder } from 'lucide-react' -import { Search, MessageSquare,ChevronDown, Plus } from 'lucide-react' +"use client" + +import React, { useState, useEffect } from 'react' +import { Search, MessageSquare, ChevronDown, Plus, Trash2, Pencil } from 'lucide-react' import Image from 'next/image' import logo from "../../../../../public/logo.png" -import Data from "../../../../data/data.json" +import { fetchChats, deleteById, renameChatById } from '@/actions/chat_actions' +import toast from 'react-hot-toast' +interface Chat { + id: string + name: string + description?: string +} const sidebar = () => { + const [chats, setChats] = useState([]) + const [loading, setLoading] = useState(true) + const [editingChatId, setEditingChatId] = useState(null) + const [editName, setEditName] = useState('') + const [searchQuery, setSearchQuery] = useState('') + + useEffect(() => { + loadChats() + }, []) + + const loadChats = async () => { + try { + setLoading(true) + const data = await fetchChats() + // Assuming the API returns chats in a specific format + // Adjust based on your actual API response structure + if (data && Array.isArray(data)) { + setChats(data) + } else if (data?.chats && Array.isArray(data.chats)) { + setChats(data.chats) + } + } catch (error) { + console.error('Error loading chats:', error) + toast.error('Failed to load chats') + } finally { + setLoading(false) + } + } + + const handleDelete = async (chatId: string) => { + if (!confirm('Are you sure you want to delete this chat?')) { + return + } + + try { + await deleteById(`/api/chat/delete?id=${chatId}`) + toast.success('Chat deleted successfully') + // Remove the chat from the list + setChats(chats.filter(chat => chat.id !== chatId)) + } catch (error) { + console.error('Error deleting chat:', error) + toast.error('Failed to delete chat') + } + } + + const handleRename = async (chatId: string) => { + if (!editName.trim()) { + toast.error('Chat name cannot be empty') + return + } + + try { + await renameChatById(chatId, editName.trim()) + toast.success('Chat renamed successfully') + // Update the chat in the list + setChats(chats.map(chat => + chat.id === chatId ? { ...chat, name: editName.trim() } : chat + )) + setEditingChatId(null) + setEditName('') + } catch (error) { + console.error('Error renaming chat:', error) + toast.error('Failed to rename chat') + } + } + + const startEditing = (chat: Chat) => { + setEditingChatId(chat.id) + setEditName(chat.name) + } + + const cancelEditing = () => { + setEditingChatId(null) + setEditName('') + } + + const filteredChats = chats.filter(chat => + chat.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + return (
{/* My Chats header */} @@ -28,6 +115,8 @@ const sidebar = () => { setSearchQuery(e.target.value)} className="bg-transparent border-none text-zinc-300 text-sm w-full focus:outline-none" />
@@ -44,18 +133,81 @@ const sidebar = () => {
- {Data.jobs.map((job) => ( -
-
-
- - {job.name} + {loading ? ( +
Loading chats...
+ ) : filteredChats.length === 0 ? ( +
No chats found
+ ) : ( + filteredChats.map((chat) => ( +
+
+ {editingChatId === chat.id ? ( +
+ + setEditName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleRename(chat.id) + } else if (e.key === 'Escape') { + cancelEditing() + } + }} + className="bg-zinc-700 text-zinc-300 text-sm px-2 py-1 rounded flex-1 focus:outline-none focus:ring-2 focus:ring-green-500" + autoFocus + /> + + +
+ ) : ( + <> +
+ + {chat.name} +
+ {chat.description && ( +

{chat.description}

+ )} + + )} +
+ {editingChatId !== chat.id && ( +
+ + +
+ )} +
+ )) + )}
-

{job.description}

-
-
- ))} -
{/* New chat button */} diff --git a/src/app/(root)/dashboard/_components/Dashboard.tsx b/src/app/(root)/dashboard/_components/Dashboard.tsx index 588de99..4de8ac1 100644 --- a/src/app/(root)/dashboard/_components/Dashboard.tsx +++ b/src/app/(root)/dashboard/_components/Dashboard.tsx @@ -2,12 +2,14 @@ import { useState, useEffect, useCallback } from "react"; import debounce from "lodash.debounce"; -import { getJobById, getJobData } from "@/actions/data_actions"; +import { getJobById, getJobData, scrapeAndCreateJobs } from "@/actions/data_actions"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; import Loader from "@/components/shared/Loader"; import { renderJobCard } from "@/components/shared/jobCard"; import AppliedJobsModal from "./appliedJobs"; +import toast from "react-hot-toast"; +import { Button } from "@/components/ui/button"; // Job portals const jobPortals = [ @@ -31,6 +33,7 @@ export default function Dashboard() { const [searchQuery, setSearchQuery] = useState(""); const [appliedJobId, setAppliedJobId] = useState('') const [appliedJob, setAppliedJob] = useState() + const [scraping, setScraping] = useState(false) // Fetch jobs from API const fetchJobs = async (page: number, portal: string, query: string) => { @@ -77,11 +80,48 @@ export default function Dashboard() { // Load more jobs const loadMoreJobs = () => setPage((prevPage) => prevPage + 1); + // Scrape and create jobs + const handleScrapeAndCreateJobs = async () => { + setScraping(true); + try { + const data = await scrapeAndCreateJobs(); + toast.success("Jobs scraped and created successfully!"); + // Refresh the jobs list after scraping + setPage(1); + setJobs([]); + fetchJobs(1, selectedPortal, searchQuery); + } catch (error: any) { + console.error("Error scraping jobs:", error); + toast.error(error?.message || "Failed to scrape and create jobs"); + } finally { + setScraping(false); + } + }; + return (
+ + {/* Scrape and Create Jobs Button */} +
+ +
+ {loading && (
diff --git a/src/app/globals.css b/src/app/globals.css index c47ccf6..5ceb4c7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -99,4 +99,33 @@ background: linear-gradient(270deg, #392c2a, #3e392b, #382017, #262d19); background-size: 800% 800%; animation: gradient 5s ease infinite; +} + + +/* Apply smooth transitions to all buttons */ +button, a { + @apply transition-all duration-300; +} + +/* Custom glowing effect for elements with glow class */ +.glow-effect { + position: relative; +} + +.glow-effect::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: inherit; + background: linear-gradient(45deg, #0070F3, rgba(0, 112, 243, 0.5), transparent); + z-index: -1; + opacity: 0; + transition: opacity 0.3s ease-in-out; +} + +.glow-effect:hover::before { + opacity: 1; } \ No newline at end of file diff --git a/src/components/ui/glowButton.tsx b/src/components/ui/glowButton.tsx new file mode 100644 index 0000000..010a9ef --- /dev/null +++ b/src/components/ui/glowButton.tsx @@ -0,0 +1,49 @@ + +import React from 'react'; +import { cn } from '@/lib/utils'; + +interface GlowButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + variant?: 'primary' | 'secondary'; + size?: 'default' | 'sm' | 'lg'; + className?: string; + leftIcon?: React.ReactNode; +} + +const GlowButton = React.forwardRef( + ({ children, variant = 'primary', size = 'default', className, leftIcon, ...props }, ref) => { + return ( + + ); + } +); + +GlowButton.displayName = 'GlowButton'; + +export default GlowButton; \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index feebab1..e84e48b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -52,31 +52,39 @@ const config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + glow: { + blue: '#53ffe9', + dark: '#121212' + } }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, - keyframes: { - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - 'slide-in-right': { - '0%': { transform: 'translateX(100%)', opacity: '0' }, - '100%': { transform: 'translateX(0)', opacity: '1' }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - 'slide-in-right': 'slide-in-right 0.5s ease-out forwards', - }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + }, }, plugins: [require("tailwindcss-animate")],