diff --git a/apps/web/package.json b/apps/web/package.json index 2abd594..7848f7c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -25,6 +25,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "framer-motion": "^11.15.0", + "geist": "^1.5.1", "lucide-react": "^0.456.0", "next": "15.5.3", "next-auth": "^4.24.11", diff --git a/apps/web/src/app/(main)/dashboard/home/page.tsx b/apps/web/src/app/(main)/dashboard/home/page.tsx index 671d0ec..9e160ca 100644 --- a/apps/web/src/app/(main)/dashboard/home/page.tsx +++ b/apps/web/src/app/(main)/dashboard/home/page.tsx @@ -1,5 +1,5 @@ "use client"; - +import React from "react"; import { useProjectTitleStore } from "@/store/useProjectTitleStore"; import Dashboard from "../page"; import { useProjectsData } from "@/store/useProjectsDataStore"; @@ -16,7 +16,7 @@ const Home = () => { const initializeState = () => { setData(projectsOfTheWeek); setRenderProjects(true); - setProjectTitle("Projects of the week"); + setProjectTitle("Featured projects"); }; initializeState(); diff --git a/apps/web/src/app/(main)/dashboard/layout.tsx b/apps/web/src/app/(main)/dashboard/layout.tsx index 8bb4abb..893a416 100644 --- a/apps/web/src/app/(main)/dashboard/layout.tsx +++ b/apps/web/src/app/(main)/dashboard/layout.tsx @@ -1,9 +1,10 @@ "use client"; -import { DashboardHeader } from "@/components/dashboard/DashboardHeader"; import Sidebar from "@/components/dashboard/Sidebar"; import FiltersContainer from "@/components/ui/FiltersContainer"; import { useFilterStore } from "@/store/useFilterStore"; import { useShowSidebar } from "@/store/useShowSidebar"; +import { IconWrapper } from "@/components/ui/IconWrapper"; +import { Bars3Icon } from "@heroicons/react/24/outline"; export default function DashboardLayout({ children, @@ -11,20 +12,23 @@ export default function DashboardLayout({ children: React.ReactNode; }) { const { showFilters } = useFilterStore(); - const { showSidebar } = useShowSidebar(); + const { showSidebar, setShowSidebar } = useShowSidebar(); return ( -
-
- -
-
- {showFilters && } - -
{children}
+
+ {showFilters && } + +
+
+ setShowSidebar(true)}> + + +

Opensox

+
+
+ {children} +
); diff --git a/apps/web/src/app/(main)/dashboard/projects/page.tsx b/apps/web/src/app/(main)/dashboard/projects/page.tsx index 8c7c023..3472f8a 100644 --- a/apps/web/src/app/(main)/dashboard/projects/page.tsx +++ b/apps/web/src/app/(main)/dashboard/projects/page.tsx @@ -4,17 +4,20 @@ import { useRenderProjects } from "@/store/useRenderProjectsStore"; import Dashboard from "../page"; import { useEffect } from "react"; import { useProjectTitleStore } from "@/store/useProjectTitleStore"; +import { useProjectsData } from "@/store/useProjectsDataStore"; const Projects = () => { const { setRenderProjects } = useRenderProjects(); const { setProjectTitle } = useProjectTitleStore(); + const { setData } = useProjectsData(); useEffect(() => { - setRenderProjects(false); - setProjectTitle("Projects of the week"); - }, [setRenderProjects, setProjectTitle]); + setRenderProjects(true); // Change to true to always render the container + setProjectTitle("Projects"); + setData([]); // Clear any existing projects + }, [setRenderProjects, setProjectTitle, setData]); - return ; + return ; }; export default Projects; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index bdba95f..7d41b87 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -10,6 +10,7 @@ import { authConfig } from "@/lib/auth/config"; import { SessionWrapper } from "./SessionWrapper"; import SupportDropdown from "@/components/landing-sections/SupportDropdown"; import { TRPCProvider } from "@/providers/trpc-provider"; +import { GeistSans } from "geist/font/sans"; const dmReg = localFont({ src: "./fonts/DMMono-Regular.ttf", @@ -39,7 +40,7 @@ export default async function RootLayout({ return ( -
- {renderProjects && ( +
+
+ {renderProjects && !loading && ( )} {loading && ( - - )} - {projectsNotFound && ( - +
+ +
)} - {!renderProjects && !loading && ( - + {projectsNotFound && !loading && ( +
+ +
)}
diff --git a/apps/web/src/components/dashboard/ProjectsContainer.tsx b/apps/web/src/components/dashboard/ProjectsContainer.tsx index 172d554..be00403 100644 --- a/apps/web/src/components/dashboard/ProjectsContainer.tsx +++ b/apps/web/src/components/dashboard/ProjectsContainer.tsx @@ -1,6 +1,7 @@ "use client"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Table, TableBody, @@ -12,32 +13,13 @@ import { import { useProjectTitleStore } from "@/store/useProjectTitleStore"; import { DashboardProjectsProps } from "@/types"; import Image from "next/image"; +import { useFilterStore } from "@/store/useFilterStore"; +import { usePathname } from "next/navigation"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; -type ProjectsContainerProps = { - projects: DashboardProjectsProps[]; -}; - -interface languageColorsTypes { - [key: string]: string; - javascript: string; - typescript: string; - python: string; - go: string; - rust: string; - java: string; - "c#": string; - "c++": string; - c: string; - php: string; - swift: string; - kotlin: string; - ruby: string; - scala: string; - html: string; - elixir: string; -} +type ProjectsContainerProps = { projects: DashboardProjectsProps[] }; -const languageColors: languageColorsTypes = { +const languageColors: Record = { javascript: "bg-yellow-500/15 text-yellow-500", typescript: "bg-blue-500/15 text-blue-500", python: "bg-emerald-500/15 text-emerald-500", @@ -56,101 +38,142 @@ const languageColors: languageColorsTypes = { elixir: "bg-purple-600/15 text-purple-600", }; -const getColor = (color: string): string => { - const lowerColorCase = color.toLowerCase(); - const _color = languageColors[lowerColorCase] || "bg-gray-200 text-gray-800"; - return _color; -}; +const getColor = (c?: string) => + languageColors[(c || "").toLowerCase()] || "bg-gray-200/10 text-gray-300"; + +const tableColumns = [ + "Project", + "Issues", + "Language", + "Popularity", + "Stage", + "Competition", + "Activity", +]; export default function ProjectsContainer({ projects, }: ProjectsContainerProps) { - const handleClick = (link: string) => { - window.open(link, "_blank"); - }; + const pathname = usePathname(); const { projectTitle } = useProjectTitleStore(); - const tableColums = [ - "Project", - "Issues", - "Language", - "Popularity", - "Stage", - "Competition", - "Activity", - ]; + const { setShowFilters } = useFilterStore(); + const isProjectsPage = pathname === "/dashboard/projects"; + return ( -
-
-

+
+
+

{projectTitle}

+ {isProjectsPage && ( + + )}
-
- - - - {tableColums.map((name, index) => ( - - {name} - - ))} - - - - {projects.map((project) => ( - { - handleClick(project.url); - }} - > - -
- -
- - {project.name} - -
- - {project.totalIssueCount} - - - 0 ? ( +
+
+ {/* Sticky header row */} + + + {tableColumns.map((name, i) => ( + - {project.primaryLanguage} - - - - {project.popularity} - - - {project.stage} - - - {project.competition} - - - {project.activity} - + {name} + + ))} - ))} - -
-
+ + + + {projects.map((p) => ( + window.open(p.url, "_blank")} + > + +
+
+ {p.name} +
+ + {p.name} + +
+
+ + + {p.totalIssueCount} + + + + + {p.primaryLanguage} + + + + + {p.popularity} + + + {p.stage} + + + {p.competition} + + + {p.activity} + +
+ ))} +
+ +
+ ) : isProjectsPage ? ( +
+
+ +

Find Your Next Project

+
+

+ Click the 'Find projects' button above to discover open + source projects that match your interests +

+
+ ) : null}

); } diff --git a/apps/web/src/components/dashboard/Sidebar.tsx b/apps/web/src/components/dashboard/Sidebar.tsx index d9f3156..eabb046 100644 --- a/apps/web/src/components/dashboard/Sidebar.tsx +++ b/apps/web/src/components/dashboard/Sidebar.tsx @@ -1,21 +1,39 @@ "use client"; +import React, { useState } from "react"; import Link from "next/link"; import SidebarItem from "../sidebar/SidebarItem"; import { usePathname } from "next/navigation"; import { IconWrapper } from "../ui/IconWrapper"; -import { XMarkIcon } from "@heroicons/react/24/outline"; +import { + XMarkIcon, + HomeIcon, + FolderIcon, + ArrowRightOnRectangleIcon, + ChevronDoubleLeftIcon, + ChevronDoubleRightIcon, + MagnifyingGlassIcon, + SparklesIcon, + StarIcon, + HeartIcon, + EnvelopeIcon, +} from "@heroicons/react/24/outline"; import { useShowSidebar } from "@/store/useShowSidebar"; import { signOut } from "next-auth/react"; +import { Twitter } from "../icons/icons"; +import { ProfilePic } from "./ProfilePic"; +import { useFilterStore } from "@/store/useFilterStore"; const SIDEBAR_ROUTES = [ { path: "/dashboard/home", label: "Home", + icon: , }, { path: "/dashboard/projects", label: "Projects", + icon: , }, ]; @@ -25,8 +43,10 @@ const getSidebarLinkClassName = (currentPath: string, routePath: string) => { }; export default function Sidebar() { - const { showSidebar, setShowSidebar } = useShowSidebar(); + const { showSidebar, setShowSidebar, isCollapsed, toggleCollapsed } = + useShowSidebar(); const pathname = usePathname(); + const { setShowFilters } = useFilterStore(); const reqFeatureHandler = () => { window.open("https://discord.gg/37ke8rYnRM", "_blank"); @@ -50,61 +70,148 @@ export default function Sidebar() { window.open(mailtoLink, "_blank"); }; - const handleLogout = () => { - signOut({ callbackUrl: "/" }); + const handleFindProjects = () => { + setShowFilters(true); }; return (
-
-
-

Opensox

-
-
- setShowSidebar(false)}> - - + {/* Mobile header */} +
+
+

Opensox AI

+ setShowSidebar(false)}> + + +
+ + {/* Desktop header with collapse */} +
+ {!isCollapsed && ( + + Opensox AI + + )} + + {isCollapsed ? ( + + ) : ( + + )} +
-
+
+ {/* Find projects entry */} + {SIDEBAR_ROUTES.map((route) => { + const activeClass = getSidebarLinkClassName(pathname, route.path); return ( - - + + ); })} + } + collapsed={isCollapsed} + /> + icon={} + collapsed={isCollapsed} + /> + icon={} + collapsed={isCollapsed} + /> + icon={} + collapsed={isCollapsed} + /> - + icon={} + collapsed={isCollapsed} + /> { window.open("https://x.com/ajeetunc", "_blank"); }} - > + icon={ + + + + } + collapsed={isCollapsed} + />
+ + {/* Bottom profile */} + +
+ ); +} + +function ProfileMenu({ isCollapsed }: { isCollapsed: boolean }) { + const [open, setOpen] = useState(false); + return ( +
+
setOpen((s) => !s)} + > + + {!isCollapsed && ( +
+
+ + Ajeet + + hi@opensox.ai +
+ +
+ )} +
+ {/* Expandable menu */} + {!isCollapsed && open && ( +
+ signOut({ callbackUrl: "/" })} + icon={} + collapsed={false} + /> +
+ )}
); } diff --git a/apps/web/src/components/sidebar/SidebarItem.tsx b/apps/web/src/components/sidebar/SidebarItem.tsx index 656a6e0..5bd2742 100644 --- a/apps/web/src/components/sidebar/SidebarItem.tsx +++ b/apps/web/src/components/sidebar/SidebarItem.tsx @@ -1,9 +1,28 @@ "use client" -export default function SidebarItem({itemName, onclick}: {itemName: string, onclick?: () => void}) { - return ( -
-

{itemName}

-
- ) +import React from "react"; + +type SidebarItemProps = { + itemName: string; + onclick?: () => void; + icon?: React.ReactNode; + collapsed?: boolean; +}; + +export default function SidebarItem({ itemName, onclick, icon, collapsed = false }: SidebarItemProps) { + return ( +
+ {icon && {icon}} + {!collapsed && ( +

+ {itemName} +

+ )} +
+ ); } \ No newline at end of file diff --git a/apps/web/src/components/ui/Filter.tsx b/apps/web/src/components/ui/Filter.tsx index 5c1340b..b091b81 100644 --- a/apps/web/src/components/ui/Filter.tsx +++ b/apps/web/src/components/ui/Filter.tsx @@ -23,39 +23,31 @@ export default function Filter({ }; const triggerClasses = clsx("text-sm font-medium", { - "text-slate-500": ["Hire contributors", "Funding", "Trending"].includes( + "text-slate-300": ["Hire contributors", "Funding", "Trending"].includes( filterName ), }); return (
- + - {filterName} + {filterName} - - + + {filters.map((filter) => ( -
+
{ - recordFilterInput(filter); - }} + onClick={() => recordFilterInput(filter)} + className="border-[#28282c] bg-[#141418] text-ox-purple transition data-[state=checked]:border-ox-purple data-[state=checked]:bg-ox-purple/20 data-[state=checked]:ring-2 data-[state=checked]:ring-ox-purple/50" /> diff --git a/apps/web/src/components/ui/FiltersContainer.tsx b/apps/web/src/components/ui/FiltersContainer.tsx index 9eb79e3..84c92fe 100644 --- a/apps/web/src/components/ui/FiltersContainer.tsx +++ b/apps/web/src/components/ui/FiltersContainer.tsx @@ -57,24 +57,29 @@ export default function FiltersContainer() { }; return ( -
+
+ {/* Backdrop */}
toggleShowFilters()} /> -
-
-

Filters

- + + {/* Filter Panel */} +
+ {/* Header */} +
+

Filters

+ toggleShowFilters()} />
-
- + {/* Filter Content */} +
+
-
+ {/* Footer */} +
diff --git a/apps/web/src/store/useShowSidebar.ts b/apps/web/src/store/useShowSidebar.ts index 622996a..04a4b0a 100644 --- a/apps/web/src/store/useShowSidebar.ts +++ b/apps/web/src/store/useShowSidebar.ts @@ -3,9 +3,14 @@ import { create } from "zustand"; interface showSidebarProps { showSidebar: boolean; setShowSidebar: (value: boolean) => void; + isCollapsed: boolean; + toggleCollapsed: () => void; } export const useShowSidebar = create((set) => ({ showSidebar: false, setShowSidebar: (value) => set({ showSidebar: value }), + isCollapsed: false, + toggleCollapsed: () => + set((state) => ({ isCollapsed: !state.isCollapsed })), })); diff --git a/apps/web/src/utils/converter.ts b/apps/web/src/utils/converter.ts index dbe8f4f..53a5c79 100644 --- a/apps/web/src/utils/converter.ts +++ b/apps/web/src/utils/converter.ts @@ -148,7 +148,7 @@ export const convertApiOutputToUserOutput = ( url: item.url, avatarUrl: item.owner.avatarUrl, totalIssueCount: item.issues.totalCount, - primaryLanguage: item.primaryLanguage.name, + primaryLanguage: item.primaryLanguage?.name || "Other", popularity: filters.Popularity ? filters.Popularity : "-", stage: filters.Stage ? filters.Stage : "-", competition: filters.Competition ? filters.Competition : "-", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46216f8..8b60240 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,9 @@ importers: framer-motion: specifier: ^11.15.0 version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + geist: + specifier: ^1.5.1 + version: 1.5.1(next@15.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) lucide-react: specifier: ^0.456.0 version: 0.456.0(react@18.3.1) @@ -2791,6 +2794,11 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + geist@1.5.1: + resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} + peerDependencies: + next: '>=13.2.0' + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4369,9 +4377,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -7531,6 +7536,10 @@ snapshots: functions-have-names@1.2.3: {} + geist@1.5.1(next@15.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + dependencies: + next: 15.5.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9276,8 +9285,6 @@ snapshots: toidentifier@1.0.1: {} - tr46@0.0.3: {} - ts-api-utils@1.4.3(typescript@5.9.2): dependencies: typescript: 5.9.2