diff --git a/Makefile b/Makefile index 23c7b68..f6021f9 100644 --- a/Makefile +++ b/Makefile @@ -21,4 +21,4 @@ deploy: build firebase deploy --only hosting clean: - rm -rf node_modules .next out \ No newline at end of file + rm -rf node_modules .next out diff --git a/package-lock.json b/package-lock.json index 934493f..4a4e65b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3781,12 +3781,12 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -5167,9 +5167,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -6800,9 +6800,9 @@ } }, "node_modules/mongoose": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", - "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.1.tgz", + "integrity": "sha512-gHSPD8qEwRmiXapK17hEnFWZdcFENMegHTcw5XIIg2+7R8eXQvdwSiMpD/A2oG8tKzFLLHyRXd8/eaDPAVwZgQ==", "license": "MIT", "dependencies": { "bson": "^6.10.4", @@ -7338,9 +7338,9 @@ } }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", diff --git a/public/amrabed.webp b/public/amrabed.webp new file mode 100644 index 0000000..af835e7 Binary files /dev/null and b/public/amrabed.webp differ diff --git a/src/app/globals.css b/src/app/globals.css index 42738f6..20f76cf 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -64,7 +64,7 @@ div { /* Section */ .section { - @apply min-h-[100vh] w-full flex flex-col overflow-x-hidden content-evenly items-center justify-evenly shadow-sm overflow-hidden; + @apply h-fit w-full flex flex-col overflow-x-hidden content-start items-center justify-start shadow-sm overflow-hidden; } .section-heading { diff --git a/src/app/page.tsx b/src/app/page.tsx index c422cdf..78c14c5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,92 +1,62 @@ -import dynamic from "next/dynamic"; -import Image from "next/image"; +"use client"; + +import { useEffect, useState } from "react"; import { Banner } from "@/components/banner"; import { MainHeader } from "@/components/header"; -import { Section } from "@/components/section"; -import certificates from "@/data/certifications"; -import degrees from "@/data/degrees"; -import skills from "@/data/skills"; -import type { Certification, Degree, Skill } from "@/types"; -import "@/types"; +import Intro from "@/components/sections/hero"; +import { AboutSection } from "@/components/sections/about"; +import { CertificationsSection } from "@/components/sections/certifications"; +import { EducationSection } from "@/components/sections/education"; +import { ExperienceSection } from "@/components/sections/experience"; +import { ProjectsSection } from "@/components/sections/projects"; +import { SkillsSection } from "@/components/sections/skills"; +import { UnifiedFilterBar } from "@/components/unified-filter-bar"; + +const Home = () => { + const [showFilter, setShowFilter] = useState(false); -const Intro = dynamic(() => import("@/components/intro")); + useEffect(() => { + const handleScroll = () => { + const skillsSection = document.getElementById("skills"); + const experienceSection = document.getElementById("experience"); -const Skills = () => ( -
- {Object.values(skills).map((skill: Skill) => ( -
-

{skill.icon}

-

{skill.name}

-
- ))} -
-); + if (skillsSection && experienceSection) { + const skillsTop = skillsSection.offsetTop; + const experienceBottom = + experienceSection.offsetTop + experienceSection.offsetHeight; -const Certifications = () => ( -
- {certificates.map((certificate: Certification) => ( -
- -
- {`Badge -

{certificate.title}

-

{certificate.organization.name}

-

{certificate.date}

-
-
-
- ))} -
-); + // Show filter bar when between skills top and experience bottom + // Adjusted to hide when reaching the About section + setShowFilter( + window.scrollY > skillsTop - 200 && + window.scrollY + window.innerHeight < experienceBottom + 100, + ); + } + }; -const Degrees = () => ( -
- {degrees.map((degree: Degree) => ( -
- -
- {`${degree.university.name} -

- {degree.title} -

-

- {degree.university.name} -

-

{degree.duration}

-
-
-
- ))} -
-); + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); -const Home = () => ( - <> - - - - - - - -); + return ( +
+ + + +
+
+ + + + + + +
+
+ {showFilter && } +
+ ); +}; export default Home; diff --git a/src/app/positions/layout.tsx b/src/app/positions/layout.tsx deleted file mode 100644 index 8c2f1fa..0000000 --- a/src/app/positions/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Amr Abed - Positions", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return children; -} diff --git a/src/app/positions/page.tsx b/src/app/positions/page.tsx deleted file mode 100644 index 4bb5abf..0000000 --- a/src/app/positions/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { Section } from "@/components/section"; -import React, { useMemo } from "react"; - -import { useFilter } from "@/contexts/filter"; -import { useSearch } from "@/contexts/search"; -import positions from "@/data/positions"; -import { filterByQuery, filterBySelection } from "@/filter"; - -import { EmptyState } from "@/components/empty-state"; - -import { FilterBase } from "../../components/filter-base"; -import Timeline from "./timeline"; - -const Page = () => { - const { debouncedQuery } = useSearch(); - const { selected } = useFilter(); - - const filteredPositions = useMemo(() => { - const lowercaseQuery = debouncedQuery.toLowerCase(); - const roleSet = new Set((selected["roles"] || []).map((s) => s.toLowerCase())); - const toolSet = new Set((selected["tools"] || []).map((s) => s.toLowerCase())); - const skillSet = new Set((selected["skills"] || []).map((s) => s.toLowerCase())); - - return positions.filter( - (position) => - filterByQuery(position, lowercaseQuery) && - filterBySelection(position.tags, skillSet) && - filterBySelection(position.roles, roleSet) && - filterBySelection(position.skills, toolSet), - ); - }, [debouncedQuery, selected]); - - return ( - -
- {filteredPositions.length > 0 ? ( -
- -
- ) : ( - - )} -
-
- ); -}; - -export default Page; diff --git a/src/app/projects/layout.tsx b/src/app/projects/layout.tsx deleted file mode 100644 index 33e848d..0000000 --- a/src/app/projects/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Amr Abed - Projects", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return children; -} diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx deleted file mode 100644 index 136aa65..0000000 --- a/src/app/projects/page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { Section } from "@/components/section"; -import React, { useMemo } from "react"; - -import { useFilter } from "@/contexts/filter"; -import { useSearch } from "@/contexts/search"; -import projects from "@/data/projects"; -import { filterByQuery, filterBySelection } from "@/filter"; -import type { Project } from "@/types"; - -import { EmptyState } from "@/components/empty-state"; - -import { FilterBase } from "../../components/filter-base"; -import ProjectView from "./project"; - -const Page = () => { - const { debouncedQuery } = useSearch(); - const { selected } = useFilter(); - - const filteredProjects = useMemo(() => { - const lowercaseQuery = debouncedQuery.toLowerCase(); - const roleSet = new Set((selected["roles"] || []).map((s) => s.toLowerCase())); - const toolSet = new Set((selected["tools"] || []).map((s) => s.toLowerCase())); - const skillSet = new Set((selected["skills"] || []).map((s) => s.toLowerCase())); - - return projects - .filter( - (project) => - filterByQuery(project, lowercaseQuery) && - filterBySelection(project.roles, roleSet) && - filterBySelection(project.tags, skillSet) && - filterBySelection(project.tools, toolSet), - ) - .sort( - (project1, project2) => - project1.group - project2.group || - new Date(project2.date).getFullYear() - - new Date(project1.date).getFullYear(), - ); - }, [debouncedQuery, selected]); - - return ( - -
- {filteredProjects.length > 0 ? ( -
- {filteredProjects.map((project: Project) => ( - - ))} -
- ) : ( - - )} -
-
- ); -}; - -export default Page; diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 239e06f..5fd3a03 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -17,18 +17,6 @@ export default async function sitemap(): Promise { changeFrequency: "yearly", priority: 1, }, - { - url: "https://amrabed.com/projects", - lastModified: new Date(), - changeFrequency: "yearly", - priority: 0.8, - }, - { - url: "https://amrabed.com/positions", - lastModified: new Date(), - changeFrequency: "yearly", - priority: 0.5, - }, ]; try { diff --git a/src/components/filter-base.tsx b/src/components/filter-base.tsx deleted file mode 100644 index 60ce3cf..0000000 --- a/src/components/filter-base.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ReactNode, Fragment } from "react"; - -import { Selections, Filter } from "@/components/filter"; -import { PageHeader } from "@/components/header"; -import { useFilter } from "@/contexts/filter"; -import { useSearch } from "@/contexts/search"; -import areas from "@/data/areas"; -import roles from "@/data/roles"; -import skills from "@/data/skills"; - -interface FilterBaseProps { - title: string; - placeholder: string; - children: ReactNode; -} - -export const FilterBase = ({ - title, - placeholder, - children, -}: FilterBaseProps) => { - const { query, setQuery } = useSearch(); - const { selected, setSelected } = useFilter(); - - const selectedRoles = selected["roles"] || []; - const selectedTools = selected["tools"] || []; - const selectedSkills = selected["skills"] || []; - - return ( - - - - role.name)} - selected={selectedRoles} - setSelected={(values) => setSelected("roles", values as string[])} - /> - skill.name)} - selected={selectedTools} - setSelected={(values) => setSelected("tools", values as string[])} - /> - area.name)} - selected={selectedSkills} - setSelected={(values) => setSelected("skills", values as string[])} - /> - - - {children} - - ); -}; diff --git a/src/components/filter.tsx b/src/components/filter.tsx index e8a9e93..59aa1df 100644 --- a/src/components/filter.tsx +++ b/src/components/filter.tsx @@ -17,7 +17,7 @@ import { VisuallyHidden } from "@react-aria/visually-hidden"; const checkbox = tv({ slots: { - base: "border-none bg-default-100 hover:bg-default-200", + base: "border-none bg-default-100 hover:bg-default-200 dark:bg-slate-700/50 dark:hover:bg-slate-700", content: "text-foreground-500 hover:text-foreground", }, variants: { @@ -70,21 +70,28 @@ export const Selections = ({ setSelected, }: { label: string; - values: string[]; + values: { id: string; name: string; icon?: ReactNode }[]; selected: string[]; setSelected: (values: string[]) => void; }) => (
setSelected(values as string[])} > - +
- {values.map((value) => ( - - {value} + {values.map((v) => ( + +
+ {v.icon && ( + {v.icon} + )} + {v.name} +
))}
@@ -92,7 +99,13 @@ export const Selections = ({
); -export const Filter = ({ children }: { children: ReactNode }) => ( +export const Filter = ({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) => ( - + {children} diff --git a/src/components/header.tsx b/src/components/header.tsx index 0587c42..4b9735d 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,53 +1,91 @@ "use client"; -import Link from "next/link"; - -import { ReactNode, useState } from "react"; - -import { ChevronLeftIcon } from "@heroicons/react/24/outline"; -import { Separator } from "@heroui/react"; - -import { Searchbar, SearchIcon } from "./search"; +import { useEffect, useState } from "react"; export const sections = [ - { - name: "skills", - link: "#skills", - }, - { - name: "certifications", - link: "#certifications", - }, - { - name: "degrees", - link: "#degrees", - }, + { name: "Skills", link: "#skills" }, + { name: "Certifications", link: "#certifications" }, + { name: "Projects", link: "#projects" }, + { name: "Experience", link: "#experience" }, + { name: "Education", link: "#degrees" }, + { name: "About", link: "#about" }, ]; -export const pages = [ - { - name: "projects", - link: "./projects", - }, - { - name: "positions", - link: "./positions", - }, -]; - -const Title = () => ( - +const Title = ({ onClick }: { onClick: (e: React.MouseEvent) => void }) => ( + ); export const MainHeader = () => { const [isMenuOpen, setIsMenuOpen] = useState(false); + const [activeSection, setActiveSection] = useState(""); + + useEffect(() => { + const observerOptions = { + root: null, + rootMargin: "-30% 0px -60% 0px", + threshold: 0, + }; + + const observerCallback = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + if (entry.target.id === "home") { + setActiveSection(""); + } else { + setActiveSection(entry.target.id); + } + } + }); + }; + + const observer = new IntersectionObserver( + observerCallback, + observerOptions, + ); + + sections.forEach((section) => { + const element = document.getElementById(section.link.substring(1)); + if (element) observer.observe(element); + }); + + const home = document.getElementById("home"); + if (home) observer.observe(home); + + return () => observer.disconnect(); + }, []); + + const handleScroll = (e: React.MouseEvent, link: string) => { + e.preventDefault(); + const targetId = link.substring(1); + const element = document.getElementById(targetId); + if (element) { + window.scrollTo({ + top: element.offsetTop - 100, // Adjusted offset for better centering + behavior: "smooth", + }); + setIsMenuOpen(false); + } + }; + + const handleScrollToTop = (e: React.MouseEvent) => { + e.preventDefault(); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + setIsMenuOpen(false); + setActiveSection(""); + }; return ( -