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) => (
-
- ))}
-
-);
+ // 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) => (
-
- ))}
-
-);
+ 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 (
-