Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
814 changes: 482 additions & 332 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"framer-motion": "^12.23.0",
"ogl": "^1.0.11",
"prop-types": "^15.8.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
Expand Down
2 changes: 1 addition & 1 deletion public/rel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Hero from './components/Hero'
import TechStack from './components/TechStack'
import Experience from './pages/Experience'
import Projects from './pages/Projects'
import Github from './pages/Github'

export default function App() {
return (
Expand All @@ -11,11 +12,14 @@ export default function App() {
<main className="pt-20">
<Hero />
<TechStack />
<section id="experience" className="py-20">
<section id="experience" className="py-10">
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-16">Experience</h2>
<Experience />
</section>
<section id="projects" className="py-20">
<section id="github" className="py-10">
<Github />
</section>
<section id="projects" className="py-10">
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-16">Projects</h2>
<Projects />
</section>
Expand Down
6 changes: 6 additions & 0 deletions src/components/Button.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import PropTypes from "prop-types";

function Button(props) {
return (
<button className='border-2 border-black px-2 rounded-md font-mono hover:bg-gray-100 transition'>
Expand All @@ -6,4 +8,8 @@ function Button(props) {
)
}

Button.propTypes = {
name: PropTypes.string.isRequired,
};

export default Button
9 changes: 9 additions & 0 deletions src/components/CircularGallery.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Camera, Mesh, Plane, Program, Renderer, Texture, Transform } from "ogl";
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";

function debounce(func, wait) {
let timeout;
Expand Down Expand Up @@ -381,3 +382,11 @@ export default function CircularGallery({
}, [items, bend, borderRadius, scrollSpeed, scrollEase]);
return <div className="w-full h-full overflow-hidden cursor-grab active:cursor-grabbing" ref={containerRef} />;
}

CircularGallery.propTypes = {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
bend: PropTypes.number,
borderRadius: PropTypes.number,
scrollSpeed: PropTypes.number,
scrollEase: PropTypes.number,
};
14 changes: 14 additions & 0 deletions src/components/CountUp.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from "framer-motion";
import PropTypes from "prop-types";

export default function CountUp({
to,
Expand Down Expand Up @@ -79,3 +80,16 @@ export default function CountUp({

return <span className={`${className}`} ref={ref} />;
}

CountUp.propTypes = {
to: PropTypes.number.isRequired,
from: PropTypes.number,
direction: PropTypes.oneOf(["up", "down"]),
delay: PropTypes.number,
duration: PropTypes.number,
className: PropTypes.string,
startWhen: PropTypes.bool,
separator: PropTypes.string,
onStart: PropTypes.func,
onEnd: PropTypes.func,
};
7 changes: 7 additions & 0 deletions src/components/HamburgerMenu.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';

const HamburgerMenu = ({ isOpen, isDark, onClick }) => {
return (
Expand All @@ -18,6 +19,12 @@ const HamburgerMenu = ({ isOpen, isDark, onClick }) => {
);
}

HamburgerMenu.propTypes = {
isOpen: PropTypes.bool.isRequired,
isDark: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired,
};

const StyledWrapper = styled.div`
.hamburger {
cursor: pointer;
Expand Down
13 changes: 12 additions & 1 deletion src/components/ProjectCard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Arrow } from "./Assets";
import { Code } from "./Assets";
import CountUp from "./CountUp";
Expand All @@ -8,7 +9,6 @@ const ProjectCard = (props) => {

useEffect(() => {
if (props.progress) {
let start = 0;
const duration = 2500;
const startTime = performance.now();

Expand Down Expand Up @@ -85,4 +85,15 @@ const ProjectCard = (props) => {
);
};

ProjectCard.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
techStack: PropTypes.arrayOf(PropTypes.string),
codeLink: PropTypes.string,
liveLink: PropTypes.string,
startDate: PropTypes.string,
endDate: PropTypes.string,
progress: PropTypes.number,
};

export default ProjectCard;
8 changes: 4 additions & 4 deletions src/components/SocialMedia.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ export default function SocialMedia() {
return (
<div className="card">
<a href="mailto:rushilpatel.cse@gmail.com">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-5">
<path d="M3 4a2 2 0 0 0-2 2v1.161l8.441 4.221a1.25 1.25 0 0 0 1.118 0L19 7.162V6a2 2 0 0 0-2-2H3Z" />
<path d="m19 8.839-7.77 3.885a2.75 2.75 0 0 1-2.46 0L1 8.839V14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.839Z" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-5">
<path d="M3 4a2 2 0 0 0-2 2v1.161l8.441 4.221a1.25 1.25 0 0 0 1.118 0L19 7.162V6a2 2 0 0 0-2-2H3Z" />
<path d="m19 8.839-7.77 3.885a2.75 2.75 0 0 1-2.46 0L1 8.839V14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.839Z" />
</svg>
Expand Down Expand Up @@ -43,10 +43,10 @@ export default function SocialMedia() {

<a href="https://stackoverflow.com/users/25128671/rushil-patel" target='_blank' rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path fill-rule="evenodd" d="M 40.925781 1.890625 L 37.859375 2.417969 L 41.1875 20.625 L 44.03125 20.253906 Z M 29.96875 6.351563 L 27.101563 8.078125 L 37.300781 23.035156 L 39.820313 21.480469 Z M 20.796875 15.03125 L 19.113281 17.703125 L 34.5 27 L 35.902344 24.578125 Z M 16.375 24.402344 L 15.628906 27.402344 L 33.359375 31.894531 L 33.640625 29.203125 Z M 9 29 L 9 47.984375 L 38.902344 48 L 38.902344 47.984375 C 38.933594 47.984375 39 29 39 29 L 36 29 L 36 45 L 12 45 L 12 29 Z M 15.152344 32.355469 L 14.902344 35.339844 L 33 37 L 33.203125 34.5 Z M 14.902344 39 L 15 42 L 33 41.929688 L 33 39 Z"></path>
<path fillRule="evenodd" d="M 40.925781 1.890625 L 37.859375 2.417969 L 41.1875 20.625 L 44.03125 20.253906 Z M 29.96875 6.351563 L 27.101563 8.078125 L 37.300781 23.035156 L 39.820313 21.480469 Z M 20.796875 15.03125 L 19.113281 17.703125 L 34.5 27 L 35.902344 24.578125 Z M 16.375 24.402344 L 15.628906 27.402344 L 33.359375 31.894531 L 33.640625 29.203125 Z M 9 29 L 9 47.984375 L 38.902344 48 L 38.902344 47.984375 C 38.933594 47.984375 39 29 39 29 L 36 29 L 36 45 L 12 45 L 12 29 Z M 15.152344 32.355469 L 14.902344 35.339844 L 33 37 L 33.203125 34.5 Z M 14.902344 39 L 15 42 L 33 41.929688 L 33 39 Z"></path>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 50 50">
<path fill-rule="evenodd" d="M 40.925781 1.890625 L 37.859375 2.417969 L 41.1875 20.625 L 44.03125 20.253906 Z M 29.96875 6.351563 L 27.101563 8.078125 L 37.300781 23.035156 L 39.820313 21.480469 Z M 20.796875 15.03125 L 19.113281 17.703125 L 34.5 27 L 35.902344 24.578125 Z M 16.375 24.402344 L 15.628906 27.402344 L 33.359375 31.894531 L 33.640625 29.203125 Z M 9 29 L 9 47.984375 L 38.902344 48 L 38.902344 47.984375 C 38.933594 47.984375 39 29 39 29 L 36 29 L 36 45 L 12 45 L 12 29 Z M 15.152344 32.355469 L 14.902344 35.339844 L 33 37 L 33.203125 34.5 Z M 14.902344 39 L 15 42 L 33 41.929688 L 33 39 Z"></path>
<path fillRule="evenodd" d="M 40.925781 1.890625 L 37.859375 2.417969 L 41.1875 20.625 L 44.03125 20.253906 Z M 29.96875 6.351563 L 27.101563 8.078125 L 37.300781 23.035156 L 39.820313 21.480469 Z M 20.796875 15.03125 L 19.113281 17.703125 L 34.5 27 L 35.902344 24.578125 Z M 16.375 24.402344 L 15.628906 27.402344 L 33.359375 31.894531 L 33.640625 29.203125 Z M 9 29 L 9 47.984375 L 38.902344 48 L 38.902344 47.984375 C 38.933594 47.984375 39 29 39 29 L 36 29 L 36 45 L 12 45 L 12 29 Z M 15.152344 32.355469 L 14.902344 35.339844 L 33 37 L 33.203125 34.5 Z M 14.902344 39 L 15 42 L 33 41.929688 L 33 39 Z"></path>
</svg>
</a>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/components/ThemeContext.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createContext, useState, useEffect } from 'react';
import PropTypes from 'prop-types';

export const ThemeContext = createContext();

Expand All @@ -25,4 +26,8 @@ export const ThemeProvider = ({ children }) => {
{children}
</ThemeContext.Provider>
);
};

ThemeProvider.propTypes = {
children: PropTypes.node.isRequired,
};
170 changes: 170 additions & 0 deletions src/pages/Github.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useEffect, useState } from 'react';
import { fetchPRs } from '../services/githubService';
import { FaCodeBranch, FaGithub } from 'react-icons/fa';

const ITEMS_PER_PAGE = 6;

export default function Github() {
const [prs, setPrs] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [userFilter, setUserFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);

useEffect(() => {
const loadPRs = async () => {
const data = await fetchPRs();
setPrs(data);
setLoading(false);
};
loadPRs();
}, []);

const filteredPrs = prs.filter(pr => {
const searchMatch = searchQuery === '' || pr.title.toLowerCase().includes(searchQuery.toLowerCase());
const userMatch = userFilter === 'all' || (userFilter === 'personal' && pr.user.login === 'rushil-b-patel') || (userFilter === 'work' && pr.user.login === 'rusp-odoo');
let statusMatch = true;
if (statusFilter === 'open') statusMatch = pr.state === 'open';
else if (statusFilter === 'closed') statusMatch = pr.state === 'closed' && !pr.mergedAt;
else if (statusFilter === 'merged') statusMatch = !!pr.mergedAt;

return searchMatch && userMatch && statusMatch;
});

const visiblePrs = filteredPrs.slice(0, visibleCount);
const hasMore = visibleCount < filteredPrs.length;

const handleLoadMore = () => {
setVisibleCount(prev => prev + ITEMS_PER_PAGE);
};

useEffect(() => {
setVisibleCount(ITEMS_PER_PAGE);
}, [searchQuery, userFilter, statusFilter]);

const getStatusStyle = (status) => {
switch (status) {
case 'open':
return 'border-green-600 text-green-700 dark:text-green-400';
case 'merged':
return 'border-purple-600 text-purple-700 dark:text-purple-400';
default:
return 'border-red-600 text-red-700 dark:text-red-400';
}
};

return (
<div className="max-w-3xl mx-auto px-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8 gap-4">
<div className="flex items-center gap-3">
<FaGithub className="text-2xl text-gray-900 dark:text-white" />
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Pull Requests
</h2>
</div>
<div className="flex items-center gap-2 flex-wrap">
<input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="px-3 py-1.5 text-xs rounded-lg border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-gray-700 dark:text-gray-300 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-gray-300 dark:focus:ring-neutral-600 w-32 sm:w-40"
/>
<select
value={userFilter}
onChange={(e) => setUserFilter(e.target.value)}
className="px-3 py-1.5 text-xs rounded-lg border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-gray-700 dark:text-gray-300 focus:outline-none"
>
<option value="all">All Users</option>
<option value="personal">Personal</option>
<option value="work">Work</option>
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-1.5 text-xs rounded-lg border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-gray-700 dark:text-gray-300 focus:outline-none"
>
<option value="all">All Status</option>
<option value="open">Open</option>
<option value="merged">Merged</option>
<option value="closed">Closed</option>
</select>
</div>
</div>

{loading ? (
<div className="flex justify-center py-10">
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-gray-900 dark:border-white" />
</div>
) : (
<>
<div className="space-y-4">
{visiblePrs.map(pr => {
const isMerged = !!pr.mergedAt;
const status = pr.state === 'open' ? 'open' : isMerged ? 'merged' : 'closed';

return (
<a
key={pr.id}
href={pr.url}
target="_blank"
rel="noopener noreferrer"
className="block p-4 sm:p-5 bg-white dark:bg-neutral-900 border border-gray-100 dark:border-neutral-800 rounded-xl hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-start justify-between gap-3 mb-4">
<h3 className="text-base font-semibold text-gray-900 dark:text-white group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors leading-snug">
{pr.title}
</h3>
<img
src={pr.user.avatar_url}
alt={pr.user.login}
className="w-7 h-7 rounded-full border border-gray-200 dark:border-neutral-700 flex-shrink-0"
title={pr.user.login}
/>
</div>

<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-gray-100 dark:bg-neutral-800 text-gray-700 dark:text-gray-300 rounded-md border border-gray-200 dark:border-neutral-700">
<FaCodeBranch className="mr-1 text-[10px]" />
{pr.repo}
</span>
<span className="px-2 py-0.5 text-xs font-medium bg-gray-100 dark:bg-neutral-800 text-gray-500 dark:text-gray-400 rounded-md border border-gray-200 dark:border-neutral-700">
{new Date(pr.createdAt).toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</span>
</div>
<span className={`px-2.5 py-0.5 text-xs font-semibold border rounded-md capitalize ${getStatusStyle(status)}`}>
{status}
</span>
</div>
</a>
);
})}
</div>

{hasMore && (
<div className="flex justify-center mt-8">
<button
onClick={handleLoadMore}
className="px-6 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-neutral-800 rounded-lg border border-gray-200 dark:border-neutral-700 hover:bg-gray-200 dark:hover:bg-neutral-700 transition-colors"
>
Load More ({filteredPrs.length - visibleCount} remaining)
</button>
</div>
)}

{filteredPrs.length === 0 && (
<div className="text-center py-10 text-gray-500 dark:text-gray-400">
No pull requests match the selected filters.
</div>
)}
</>
)}
</div>
);
}
15 changes: 8 additions & 7 deletions src/pages/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import Switch from "../components/Switch";
import { ThemeContext } from "../components/ThemeContext";
import HamburgerMenu from "../components/HamburgerMenu";

const navLinks = [
{ name: "Home", href: "#home" },
{ name: "Tech-Stack", href: "#tech-stack" },
{ name: "Experience", href: "#experience" },
{ name: "Projects", href: "#projects" },
{ name: "PR's", href: "#github" },
];

function Navbar() {
const { theme } = useContext(ThemeContext);
const [menuOpen, setMenuOpen] = useState(false);
Expand All @@ -13,13 +21,6 @@ function Navbar() {
setMenuOpen(!menuOpen);
};

const navLinks = [
{ name: "Home", href: "#home" },
{ name: "Tech-Stack", href: "#tech-stack" },
{ name: "Experience", href: "#experience" },
{ name: "Projects", href: "#projects" },
];

useEffect(() => {
const handleScroll = () => {
const sections = navLinks.map(link => document.querySelector(link.href));
Expand Down
Loading