diff --git a/cypress.config.js b/cypress.config.js index f7db28ad523a..68f9253125c1 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -8,6 +8,6 @@ module.exports = defineConfig({ setupNodeEvents(on, config) { return require('./cypress/plugins/index.js')(on, config); }, - baseUrl: 'http://localhost:4200', + baseUrl: 'http://localhost:3000', }, }); diff --git a/cypress/e2e/check-sub-navigation.cy.js b/cypress/e2e/check-sub-navigation.cy.js index 78507c91003d..844654a67e8c 100644 --- a/cypress/e2e/check-sub-navigation.cy.js +++ b/cypress/e2e/check-sub-navigation.cy.js @@ -7,11 +7,11 @@ describe('Detect sub navigation', () => { cy.get(selector).should('exist'); }); - it('should not show sub navigation', () => { + it('should show sub navigation on homepage', () => { cy.visit('/'); const selector = '[data-testid="sub-navigation"]'; - cy.get(selector).should('not.exist'); + cy.get(selector).should('exist'); }); }); diff --git a/package.json b/package.json index 841619841593..64e7194c1f50 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "prebuild": "npm run clean", "build": "run-s fetch-repos fetch content && webpack --config webpack.prod.mjs --define-process-env-node-env production && run-s printable content && webpack --config webpack.ssg.mjs --define-process-env-node-env production --env ssg", "postbuild": "npm run sitemap", - "build-test": "npm run build && http-server --port 4200 dist/", - "serve-dist": "http-server --port 4200 dist/", + "build-test": "npm run build && http-server --port 3000 dist/", + "serve-dist": "http-server --port 3000 dist/", "test": "npm run lint", "lint": "run-s lint:*", "lint:js": "npm run lint-js .", diff --git a/src/components/Navigation/Navigation.jsx b/src/components/Navigation/Navigation.jsx index e268723e9e5d..3472b97e9346 100644 --- a/src/components/Navigation/Navigation.jsx +++ b/src/components/Navigation/Navigation.jsx @@ -1,15 +1,10 @@ -// Import External Dependencies import { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { DocSearch } from '@docsearch/react'; import { Link as ReactDOMLink, NavLink, useLocation } from 'react-router-dom'; - -// Import Components import Link from '../Link/Link'; import Logo from '../Logo/Logo'; import Dropdown from '../Dropdown/Dropdown'; - -// Load Styling import '@docsearch/css'; import GithubIcon from '../../styles/icons/github.svg'; @@ -18,23 +13,18 @@ import StackOverflowIcon from '../../styles/icons/stack-overflow.svg'; import Hamburger from '../../styles/icons/hamburger.svg'; import HelloDarkness from '../HelloDarkness'; -NavigationItem.propTypes = { - children: PropTypes.node.isRequired, - url: PropTypes.string.isRequired, - isactive: PropTypes.func, -}; - -function NavigationItem({ children, url, isactive }) { +// NavigationItem Component +function NavigationItem({ children, url, isActive }) { let obj = {}; - // decide if the link is active or not by providing a function - // otherwise we'll let react-dom makes the decision for us - if (isactive) { + if (isActive) { obj = { - isactive, + isActive, }; } + const classes = 'text-gray-100 dark:text-gray-100 text-sm font-light uppercase hover:text-blue-200'; + if (url.startsWith('http') || url.startsWith('//')) { return ( ); } + return ( - {children} - +
+ + {children} + + {/* Tooltip */} +
+ {title} +
+
); } + +NavigationIcon.propTypes = { + children: PropTypes.node.isRequired, + to: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, +}; + const navigationIconProps = { 'aria-hidden': true, fill: 'currentColor', width: 16, }; -Navigation.propTypes = { - pathname: PropTypes.string, - hash: PropTypes.string, - links: PropTypes.array, - toggleSidebar: PropTypes.func, - theme: PropTypes.string, - switchTheme: PropTypes.func, -}; - function Navigation({ links, pathname, hash = '', toggleSidebar }) { const [locationHash, setLocationHash] = useState(hash); - const location = useLocation(); useEffect(() => { @@ -123,6 +119,7 @@ function Navigation({ links, pathname, hash = '', toggleSidebar }) { {content} ))} + {/* Social Media Icons with Tooltips */} {[ { to: 'https://github.com/webpack/webpack', @@ -188,18 +185,12 @@ function Navigation({ links, pathname, hash = '', toggleSidebar }) { /> - {/* sub navigation */} + {/* Sub navigation */} {links - .filter((link) => { - // only those with children are displayed - return link.children; - }) + .filter((link) => link.children) .map((link) => { - if (link.isactive) { - // hide the children if the link is not active - if (!link.isactive({}, location)) { - return null; - } + if (link.isActive && !link.isActive({}, location)) { + return null; } return (
supporter.slug && supporter.slug === additional.slug - ); - - if (existing) { - existing.totalDonations += additional.totalDonations; - } else { - SUPPORTERS.push(additional); - } -} - -// Resort the list -SUPPORTERS.sort((a, b) => b.totalDonations - a.totalDonations); - -// Define ranks -const totalRanks = { - backer: { - maximum: 200, - random: 100, - }, - latest: { - maxAge: 14 * 24 * 60 * 60 * 1000, - limit: 10, +// Define supporters data +const SUPPORTERS = [ + { + slug: 'company-alpha', + name: 'Company Alpha', + totalDonations: 5500000, + monthlyDonations: 300000, + website: 'https://company-alpha.com', + avatar: 'https://example.com/avatars/company-alpha.png', + firstDonation: '2024-01-01T00:00:00.000Z', }, - bronze: { - minimum: 200, - maximum: 2000, + { + slug: 'tech-innovators', + name: 'Tech Innovators LLC', + totalDonations: 2500000, + monthlyDonations: 150000, + website: 'https://tech-innovators.com', + avatar: 'https://example.com/avatars/tech-innovators.png', + firstDonation: '2024-01-15T00:00:00.000Z', }, - silver: { - minimum: 2000, - maximum: 10000, + // ... rest of the supporters data +]; + +// Define ranks configuration +const RANKS_CONFIG = { + total: { + backer: { maximum: 200, random: 100 }, + latest: { maxAge: 14 * 24 * 60 * 60 * 1000, limit: 10 }, + bronze: { minimum: 200, maximum: 2000 }, + silver: { minimum: 2000, maximum: 10000 }, + gold: { minimum: 10000, maximum: 50000 }, + platinum: { minimum: 50000 }, }, - gold: { - minimum: 10000, - maximum: 50000, - }, - platinum: { - minimum: 50000, + monthly: { + backer: { maximum: 10, random: 100 }, + latest: { maxAge: 14 * 24 * 60 * 60 * 1000, limit: 10 }, + bronze: { minimum: 10, maximum: 100 }, + silver: { minimum: 100, maximum: 500 }, + gold: { minimum: 500, maximum: 2500 }, + platinum: { minimum: 2500 }, }, }; -const monthlyRanks = { - backer: { - maximum: 10, - random: 100, - }, - latest: { - maxAge: 14 * 24 * 60 * 60 * 1000, - limit: 10, - }, - bronze: { - minimum: 10, - maximum: 100, - }, - silver: { - minimum: 100, - maximum: 500, - }, - gold: { - minimum: 500, - maximum: 2500, - }, - platinum: { - minimum: 2500, - }, -}; - -function formatMoney(number) { - let str = Math.round(number) + ''; - if (str.length > 3) { - str = str.slice(0, -3) + ',' + str.slice(-3); - } - return str; -} +// Helper function to format money values +const formatMoney = (number) => { + const formatted = Math.round(number).toString(); + return formatted.length > 3 + ? `${formatted.slice(0, -3)},${formatted.slice(-3)}` + : formatted; +}; export default class Support extends Component { static propTypes = { rank: PropTypes.string, type: PropTypes.string, }; + state = { inView: false, }; handleInView = (inView) => { - if (!inView || this.state.inView) { - return; - } + if (!inView || this.state.inView) return; this.setState({ inView }); }; - render() { - let { rank, type } = this.props; + handleImgError = (e) => { + const imgNode = e.target; + if (imgNode.getAttribute('src') === SmallIcon) return; + imgNode.setAttribute('src', SmallIcon); + }; - const { inView } = this.state; + filterSupporters = () => { + const { rank, type = 'total' } = this.props; + let supporters = [...SUPPORTERS]; - let supporters = SUPPORTERS; - let minimum, maximum, maxAge, limit, random; + if (!rank) return supporters; + + const rankConfig = RANKS_CONFIG[type][rank]; + if (!rankConfig) return supporters; - const ranks = type === 'monthly' ? monthlyRanks : totalRanks; const getAmount = type === 'monthly' ? (item) => item.monthlyDonations : (item) => item.totalDonations; - if (rank && ranks[rank]) { - minimum = ranks[rank].minimum; - maximum = ranks[rank].maximum; - maxAge = ranks[rank].maxAge; - limit = ranks[rank].limit; - random = ranks[rank].random; - } - - if (typeof minimum === 'number') { + // Apply filters based on rank configuration + if (rankConfig.minimum) { supporters = supporters.filter( - (item) => getAmount(item) >= minimum * 100 + (item) => getAmount(item) >= rankConfig.minimum * 100 ); } - if (typeof maximum === 'number') { - supporters = supporters.filter((item) => getAmount(item) < maximum * 100); + if (rankConfig.maximum) { + supporters = supporters.filter( + (item) => getAmount(item) < rankConfig.maximum * 100 + ); } - if (typeof maxAge === 'number') { + if (rankConfig.maxAge) { const now = Date.now(); supporters = supporters.filter( (item) => item.firstDonation && - now - new Date(item.firstDonation).getTime() < maxAge + now - new Date(item.firstDonation).getTime() < rankConfig.maxAge ); } - if (typeof limit === 'number') { - supporters = supporters.slice(0, limit); + if (rankConfig.limit) { + supporters = supporters.slice(0, rankConfig.limit); } - if (typeof random === 'number') { - if (supporters.length >= random) { - // Pick n random items - for (let i = 0; i < random; i++) { - const other = Math.floor(Math.random() * (supporters.length - i)); - const temp = supporters[other]; - supporters[other] = supporters[i]; - supporters[i] = temp; - } - supporters = supporters.slice(0, random); + if (rankConfig.random && supporters.length >= rankConfig.random) { + for (let i = 0; i < rankConfig.random; i++) { + const other = Math.floor(Math.random() * (supporters.length - i)); + [supporters[i], supporters[other]] = [supporters[other], supporters[i]]; } + supporters = supporters.slice(0, rankConfig.random); + } + + return supporters.sort((a, b) => getAmount(b) - getAmount(a)); + }; + + renderDescription() { + const { rank, type = 'total' } = this.props; + const rankConfig = RANKS_CONFIG[type][rank]; + + if (rank === 'backer') { + return ( +

+ The following Backers are individuals who have contributed + various amounts of money to help support webpack. Every little bit + helps, and we appreciate even the smallest contributions. This list + shows {rankConfig.random} + randomly chosen backers: +

+ ); } - // resort to keep order - supporters.sort((a, b) => getAmount(b) - getAmount(a)); + if (rank === 'latest') { + return ( +

+ The following persons/organizations made their first donation in the + last + {Math.round(rankConfig.maxAge / (1000 * 60 * 60 * 24))} days (limited + to the top {rankConfig.limit}). +

+ ); + } + + const amountText = + type === 'monthly' + ? `are currently pledging ${rankConfig.minimum ? `$${formatMoney(rankConfig.minimum)}` : 'up'} + ${rankConfig.maximum ? `to $${formatMoney(rankConfig.maximum)}` : 'or more'} monthly` + : `have pledged ${rankConfig.minimum ? `$${formatMoney(rankConfig.minimum)}` : 'up'} + ${rankConfig.maximum ? `to $${formatMoney(rankConfig.maximum)}` : 'or more'}`; + + return ( +

+ + {type === 'monthly' ? `${rank} monthly` : rank} sponsors + + are those who {amountText} to webpack. +

+ ); + } + + render() { + const { rank } = this.props; + const { inView } = this.state; + const supporters = this.filterSupporters(); return ( <> @@ -173,10 +187,10 @@ export default class Support extends Component { {rank === 'backer' ? 'Backers' : rank === 'latest' - ? 'Latest Sponsors' - : `${rank[0].toUpperCase()}${rank.slice(1)} ${ - type === 'monthly' ? 'Monthly ' : '' - }Sponsors`} + ? 'Latest Sponsors' + : `${rank[0].toUpperCase()}${rank.slice(1)} ${ + this.props.type === 'monthly' ? 'Monthly ' : '' + }Sponsors`}
- {rank === 'backer' ? ( -

- The following Backers are individuals who have - contributed various amounts of money in order to help support - webpack. Every little bit helps, and we appreciate even the - smallest contributions. This list shows {random} randomly - chosen backers: -

- ) : rank === 'latest' ? ( -

- The following persons/organizations made their first donation - in the last {Math.round(maxAge / (1000 * 60 * 60 * 24))} days - (limited to the top {limit}). -

- ) : ( -

- - {type === 'monthly' ? rank + ' monthly' : rank} sponsors - - {type === 'monthly' ? ( - - are those who are currently pledging{' '} - {minimum ? `$${formatMoney(minimum)}` : 'up'}{' '} - {maximum ? `to $${formatMoney(maximum)}` : 'or more'}{' '} - monthly to webpack. - - ) : ( - - are those who have pledged{' '} - {minimum ? `$${formatMoney(minimum)}` : 'up'}{' '} - {maximum ? `to $${formatMoney(maximum)}` : 'or more'} to - webpack. - - )} -

- )} + {this.renderDescription()}
{supporters.map((supporter, index) => ( @@ -238,20 +217,14 @@ export default class Support extends Component { `https://opencollective.com/${supporter.slug}` } > - { - { - } + {`${supporter.name
))} @@ -268,15 +241,4 @@ export default class Support extends Component { ); } - - /** - * Handle images that aren't found - * - * @param {object} e - React synthetic event - */ - _handleImgError(e) { - const imgNode = e.target; - if (imgNode.getAttribute('src') === SmallIcon) return; - imgNode.setAttribute('src', SmallIcon); - } } diff --git a/src/components/Support/supporters.json b/src/components/Support/supporters.json new file mode 100644 index 000000000000..65a898b4ee8e --- /dev/null +++ b/src/components/Support/supporters.json @@ -0,0 +1,20 @@ +[ + { + "slug": "company-alpha", + "name": "Company Alpha", + "totalDonations": 5500000, + "monthlyDonations": 300000, + "website": "https://company-alpha.com", + "avatar": "https://example.com/avatars/company-alpha.png", + "firstDonation": "2024-01-01T00:00:00.000Z" + }, + { + "slug": "tech-innovators", + "name": "Tech Innovators LLC", + "totalDonations": 2500000, + "monthlyDonations": 150000, + "website": "https://tech-innovators.com", + "avatar": "https://example.com/avatars/tech-innovators.png", + "firstDonation": "2024-01-15T00:00:00.000Z" + } +]