Skip to content
Open
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
38 changes: 38 additions & 0 deletions src/components/__tests__/__snapshots__/sectionsMenu.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SectionsMenu renders correctly 1`] = `
<div
className="fixed right-0 top-0 p-4"
style={
Object {
"zIndex": 999999,
}
}
>
<button
className="text-white rounded-full p-3 shadow-lg transition-all duration-200 focus:outline-none"
onClick={[Function]}
style={
Object {
"background": "#f3661aff",
"transform": "scale(1)",
}
}
>
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 6h16M4 12h16M4 18h16"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
</button>
</div>
`;
91 changes: 91 additions & 0 deletions src/components/__tests__/sectionsMenu.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import { SectionsMenu } from '../sectionsMenu';

// Mock categorizedSkills similar to skills.test.js
jest.mock('../../constants/skills', () => ({
__esModule: true,
categorizedSkills: {
language: {
title: 'Programming Languages',
skills: ['javascript'],
},
frontend_dev: {
title: 'Frontend Development',
skills: ['react'],
},
},
}));

describe('SectionsMenu', () => {
let component;

beforeEach(() => {
// Mock the document.getElementById and scrollIntoView
document.getElementById = jest.fn((id) => ({
scrollIntoView: jest.fn(),
}));
component = shallow(<SectionsMenu />);
});

it('renders correctly', () => {
expect(toJson(component)).toMatchSnapshot();
});

it('starts with a closed menu', () => {
expect(component.find('.absolute').exists()).toBeFalsy();
});

it('opens menu when clicking the toggle button', () => {
// Find and click the menu button
component.find('button').first().simulate('click');
expect(component.find('.absolute').exists()).toBeTruthy();
});

it('closes menu when clicking the toggle button again', () => {
// Open menu
component.find('button').first().simulate('click');
expect(component.find('.absolute').exists()).toBeTruthy();

// Close menu
component.find('button').first().simulate('click');
expect(component.find('.absolute').exists()).toBeFalsy();
});

describe('Section navigation', () => {
beforeEach(() => {
component.find('button').first().simulate('click');
});

it('calls scrollToSection when clicking main section buttons', () => {
const mockScrollIntoView = jest.fn();
document.getElementById = jest.fn(() => ({
scrollIntoView: mockScrollIntoView,
}));

// Click the Title section button
const titleButton = component
.findWhere((node) => node.type() === 'button' && node.text().includes('Title'))
.first();
titleButton.simulate('click');

expect(document.getElementById).toHaveBeenCalledWith('title-section');
expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' });
});

it('calls scrollToSection when clicking skill subsection buttons', () => {
const mockScrollIntoView = jest.fn();
document.getElementById = jest.fn(() => ({
scrollIntoView: mockScrollIntoView,
}));

// Click a skill subsection button
const subsectionButton = component.find('.pl-4.bg-gray-50 button').first();
subsectionButton.simulate('click');

expect(document.getElementById).toHaveBeenCalledWith('skills-language-section');
expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' });
});
});
});
2 changes: 1 addition & 1 deletion src/components/addons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ const Addons = (props) => {
};

return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div id="addons-section" className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Add-ons</div>
<AddonsItem
inputId="visitors-count"
Expand Down
2 changes: 2 additions & 0 deletions src/components/layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import Header from './header';
import Footer from './footer';
import { SectionsMenu } from './sectionsMenu';

const Layout = ({ children }) => (
<div className="flex flex-col min-h-screen">
<SectionsMenu />
<header>
<Header heading="GitHub Profile README Generator" />
</header>
Expand Down
142 changes: 142 additions & 0 deletions src/components/sectionsMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { categorizedSkills } from '../constants/skills';

export const SectionsMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const [isSkillsExpanded, setIsSkillsExpanded] = useState(true);

const mainSections = [
{ id: 'title-section', label: 'Title' },
{ id: 'subtitle-section', label: 'Subtitle' },
{ id: 'work-section', label: 'Work' },
{ id: 'skills-section', label: 'Skills' },
{ id: 'social-section', label: 'Social' },
{ id: 'addons-section', label: 'Addons' },
{ id: 'support-section', label: 'Support' },
];

const skillSubSections = Object.keys(categorizedSkills).map((key) => ({
id: `skills-${key}-section`,
label: categorizedSkills[key].title,
parent: 'Skills',
}));

const sections = [...mainSections, ...skillSubSections];

const scrollToSection = (sectionId) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};

return (
<div className="fixed right-0 top-0 p-4" style={{ zIndex: 999999 }}>
<button
onClick={() => setIsOpen(!isOpen)}
className="text-white rounded-full p-3 shadow-lg transition-all duration-200 focus:outline-none"
style={{ background: '#f3661aff', transform: 'scale(1)' }}
>
{isOpen ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>

{isOpen && (
<div
className="absolute right-0 mt-2 mx-2 w-48 bg-white rounded-lg shadow-xl border border-gray-200"
style={{ maxHeight: 'calc(100vh - 100px)' }}
>
<div className="px-4 py-2 border-b border-gray-200 font-medium text-gray-700 sticky top-0 bg-white">
Sections
</div>
<ul className="py-2 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 160px)' }}>
{mainSections.map((section) => (
<li key={section.id}>
{section.label === 'Skills' ? (
<button
onClick={() => {
setIsSkillsExpanded(!isSkillsExpanded);
scrollToSection(section.id);
}}
className="w-full text-left px-4 py-2 hover:bg-gray-300 transition-colors duration-200 focus:outline-none flex items-center justify-between"
>
<span className="flex items-center">
<JumpOver16 className="mr-2" />
{section.label}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-4 w-4 transform transition-transform duration-200 ${
isSkillsExpanded ? 'rotate-180' : ''
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
) : (
<button
onClick={() => scrollToSection(section.id)}
className="w-full text-left px-4 py-2 hover:bg-gray-300 transition-colors duration-200 focus:outline-none flex items-center"
>
<JumpOver16 className="mr-2" />
{section.label}
</button>
)}
{section.label === 'Skills' && (
<div
className="overflow-hidden transition-[max-height] duration-300 ease-in-out"
style={{ maxHeight: isSkillsExpanded ? `${skillSubSections.length * 40}px` : '0' }}
>
<ul className="pl-4 bg-gray-50">
{skillSubSections.map((subSection) => (
<li key={subSection.id}>
<button
onClick={() => scrollToSection(subSection.id)}
className="w-full text-left px-4 py-1 text-sm hover:bg-gray-300 transition-colors duration-200 focus:outline-none flex items-center"
>
{subSection.label}
</button>
<hr className="my-1 border-gray-300" />
</li>
))}
</ul>
</div>
)}
</li>
))}
</ul>
</div>
)}
</div>
);
};

export function JumpOver16(props) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="1em" height="1em" {...props}>
<path fill="currentColor" d="M12 14a2 2 0 1 1 0-4a2 2 0 0 1 0 4Z"></path>
</svg>
);
}
4 changes: 2 additions & 2 deletions src/components/skills.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Skills = (props) => {
};

return (
<div className="px-2 sm:px-6 mb-10 ">
<div id="skills-section" className="px-2 sm:px-6 mb-10 relative z-[1]">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-4 flex justify-between">
Skills
<div className="relative flex">
Expand Down Expand Up @@ -70,7 +70,7 @@ const Skills = (props) => {
return filtered.length !== 0;
})
.map((key) => (
<div key={key} className="divide-y divide-gray-500">
<div key={key} id={`skills-${key}-section`} className="divide-y divide-gray-500">
<div className="text-sm sm:text-xl text-gray-900 text-left py-1">{categorizedSkills[key].title}</div>
<div className="flex justify-start items-center flex-wrap w-full mb-6 pl-4 sm:pl-10">
{categorizedSkills[key].skills
Expand Down
2 changes: 1 addition & 1 deletion src/components/social.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const Social = (props) => {
const { social, handleSocialChange } = props;
return (
<div className="px-2 sm:px-6 mb-4">
<div id="social-section" className="px-2 sm:px-6 mb-4">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Social</div>
<div className="flex flex-wrap justify-center items-center">
<div className="w-1/2 flex justify-center items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
Expand Down
2 changes: 1 addition & 1 deletion src/components/subtitle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const Subtitle = (props) => {
const { data, handleDataChange } = props;
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div id="subtitle-section" className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Subtitle</div>
<input
id="subtitle"
Expand Down
16 changes: 8 additions & 8 deletions src/components/support.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ import PropTypes from 'prop-types';
const Support = (props) => {
const { support, handleSupportChange } = props;
return (
<div className="px-2 sm:px-6 mb-4">
<div id="support-section" className="px-2 sm:px-6 mb-4">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Support</div>
<div className="flex flex-wrap justify-start items-center">
<div className="w-1/2 flex justify-start items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<div className="flex flex-col sm:flex-row justify-start items-start sm:items-center">
<div className="w-full sm:w-1/2 flex flex-col sm:flex-row justify-start items-start sm:items-center text-xxs sm:text-lg py-2 sm:py-4">
<img
src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png"
className="w-36 h-8 sm:w-52 sm:h-12 mr-1 sm:mr-4"
className="w-32 h-7 sm:w-52 sm:h-12 mb-2 sm:mb-0 sm:mr-4"
alt="buymeacoffee"
/>
<input
id="buy-me-a-coffee"
placeholder="buymeacoffee username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 focus:border-blue-700"
className="outline-none placeholder-gray-700 w-full sm:w-1/2 max-w-[200px] border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 focus:border-blue-700"
value={support.buyMeACoffee || ''}
onChange={(event) => handleSupportChange('buyMeACoffee', event)}
/>
</div>
<div className="w-1/2 flex justify-start items-center text-xxs sm:text-lg py-4 pr-2 sm:pr-0">
<div className="w-full sm:w-1/2 flex flex-col sm:flex-row justify-start items-start sm:items-center text-xxs sm:text-lg py-2 sm:py-4">
<img
src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3"
className="w-36 h-8 sm:w-52 sm:h-12 mr-1 sm:mr-4"
className="w-32 h-7 sm:w-52 sm:h-12 mb-2 sm:mb-0 sm:mr-4"
alt="buymeakofi"
/>
<input
id="buy-me-a-kofi"
placeholder="Ko-fi username"
className="outline-none placeholder-gray-700 w-32 sm:w-1/2 border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 sm:px-2 ml-2 sm:ml-0 focus:border-blue-700"
className="outline-none placeholder-gray-700 w-full sm:w-1/2 max-w-[200px] border-t-0 border-l-0 border-r-0 border solid border-gray-900 py-1 px-1 focus:border-blue-700"
value={support.buyMeAKofi || ''}
onChange={(event) => handleSupportChange('buyMeAKofi', event)}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/title.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const Title = (props) => {
const { data, prefix, handlePrefixChange, handleDataChange } = props;
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div id="title-section" className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Title</div>
<div className="flex justify-start items-center w-full text-regular text-xs sm:text-lg">
<input
Expand Down
2 changes: 1 addition & 1 deletion src/components/work.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
const Work = (props) => {
const { prefix, handlePrefixChange, data, handleDataChange, link, handleLinkChange } = props;
return (
<div className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div id="work-section" className="flex justify-center items-start flex-col w-full px-2 sm:px-6 mb-10">
<div className="text-xl sm:text-2xl font-bold font-title mt-2 mb-2">Work</div>
<div className="text-xs sm:text-lg flex flex-col sm:flex-row mb-10 justify-center sm:justify-start items-center sm:items-start w-full px-4 sm:px-0">
<input
Expand Down