diff --git a/.github/workflows/bump-meshery-version.yml b/.github/workflows/bump-meshery-version.yml new file mode 100644 index 000000000..f82332b44 --- /dev/null +++ b/.github/workflows/bump-meshery-version.yml @@ -0,0 +1,141 @@ +name: Bump Meshery, Meshery Extensions and Meshery Cloud + +on: + workflow_run: + workflows: [Publish Node.js Package] + types: + - completed + +jobs: + versions-check: + runs-on: ubuntu-latest + outputs: + current: ${{ steps.current.outputs.VERSION }} + steps: + - name: Download Version + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: version-number + github-token: ${{ secrets.GH_ACCESS_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + - name: Retrieve Version + run: | + echo "VERSION=$(cat ./number)" >> $GITHUB_OUTPUT + id: current + bump-meshery: + runs-on: ubuntu-latest + needs: versions-check + steps: + - name: Checkout Meshery code + uses: actions/checkout@v4 + with: + repository: meshery/meshery + fetch-depth: 1 + token: ${{ secrets.RELEASEDRAFTER_PAT }} + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "npm" + cache-dependency-path: '**/package-lock.json' + - name: Make changes to pull request + working-directory: ui + run: npm install @layer5/sistent@${{needs.versions-check.outputs.current}} + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.RELEASEDRAFTER_PAT }} + commit-message: Bump sistent v${{ needs.versions-check.outputs.current }} dependencies + committer: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + signoff: true + branch: bump-sistent-bot + delete-branch: true + title: '[Chore]: Bump v${{ needs.versions-check.outputs.current }}' + add-paths: | + ui/package.json + ui/package-lock.json + body: | + Update to Sistent v${{ needs.versions-check.outputs.current }} + + _This pull request has been auto-generated by [l5io](http://github.com/l5io)_ + assignees: l5io + draft: false + bump-meshery-extensions: + runs-on: ubuntu-latest + needs: versions-check + steps: + - name: Checkout Meshery Extensions code + uses: actions/checkout@v4 + with: + repository: layer5labs/meshery-extensions + fetch-depth: 1 + token: ${{ secrets.RELEASEDRAFTER_PAT }} + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "npm" + cache-dependency-path: '**/package-lock.json' + - name: Make changes to pull request + working-directory: meshmap + run: npm install @layer5/sistent@${{needs.versions-check.outputs.current}} + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.RELEASEDRAFTER_PAT }} + commit-message: Bump sistent v${{ needs.versions-check.outputs.current }} dependencies + committer: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + signoff: true + branch: bump-sistent-bot + delete-branch: true + title: '[Chore]: Bump v${{needs.versions-check.outputs.current }}' + add-paths: | + meshmap/package.json + meshmap/package-lock.json + body: | + Update to Sistent v${{ needs.versions-check.outputs.current }} + + _This pull request has been auto-generated by [l5io](http://github.com/l5io)_ + assignees: l5io + draft: false + bump-meshery-cloud: + runs-on: ubuntu-latest + needs: versions-check + steps: + - name: Checkout Meshery Extensions code + uses: actions/checkout@v4 + with: + repository: layer5io/meshery-cloud + fetch-depth: 1 + token: ${{ secrets.RELEASEDRAFTER_PAT }} + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "npm" + cache-dependency-path: '**/package-lock.json' + - name: Make changes to pull request + working-directory: ui + run: npm install @layer5/sistent@${{needs.versions-check.outputs.current}} + - name: Create Pull Request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.RELEASEDRAFTER_PAT }} + commit-message: Bump sistent v${{ needs.versions-check.outputs.current }} dependencies + committer: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + signoff: true + branch: bump-sistent-bot + delete-branch: true + title: '[Chore]: Bump v${{ needs.versions-check.outputs.current }}' + add-paths: | + ui/package.json + ui/package-lock.json + body: | + Update to Sistent v${{ needs.versions-check.outputs.current }} + + _This pull request has been auto-generated by [l5io](http://github.com/l5io)_ + assignees: l5io + draft: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c54a481a7..71339d203 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,3 +44,28 @@ jobs: npm publish --verbose env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + versions-check: + needs: publish-gpr + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + id: versions + with: + result-encoding: string + script: | + let str = `${{github.event.release.tag_name}}` + + return str.replace(/^v/, '') + - name: Save Release number + if: ${{ !cancelled() }} + run: | + mkdir -p ./version + echo ${{ steps.versions.outputs.result }} > ./version/number + - name: Upload Version Report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: version-number + path: | + version/number + retention-days: 14 \ No newline at end of file diff --git a/.github/workflows/storybook-deploy.yml b/.github/workflows/storybook-deploy.yml deleted file mode 100644 index 0e3c5c523..000000000 --- a/.github/workflows/storybook-deploy.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Deploy Storybook - -on: - push: - branches: - - "*" - pull_request: - branches: - - "*" - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [16, 18, 20] - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: npm install - - - name: Build Storybook - run: | - cd apps/design-system - npm install - npm build-storybook - - - name: Deploy Storybook - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./storybook-static diff --git a/package-lock.json b/package-lock.json index c491f76b2..0034a3902 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "@layer5/sistent", "version": "0.14.11", "dependencies": { - "lodash": "^4.17.21" + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "react-share": "^5.1.0" }, "devDependencies": { "@commitlint/cli": "^17.7.2", @@ -58,7 +60,7 @@ "mui-datatables": "*", "react": ">=17", "react-dom": ">=17", - "xstate": "^5.13.0" + "xstate": "^5.18.2" }, "peerDependenciesMeta": { "@emotion/react": { @@ -3707,7 +3709,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4411,6 +4412,12 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -8782,7 +8789,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8894,6 +8900,29 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "dependencies": { + "debug": "^2.1.3" + } + }, + "node_modules/jsonp/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/jsonp/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -11624,6 +11653,19 @@ } } }, + "node_modules/react-share": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-5.1.0.tgz", + "integrity": "sha512-OvyfMtj/0UzH1wi90OdHhZVJ6WUC/+IeWvBwppeZozwIGyAjQgyR0QXlHOrxVHVECqnGvcpBaFTXVrqouTieaw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.3.2", + "jsonp": "^0.2.1" + }, + "peerDependencies": { + "react": "^17 || ^18" + } + }, "node_modules/react-sortable-tree-patch-react-17": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-sortable-tree-patch-react-17/-/react-sortable-tree-patch-react-17-2.9.0.tgz", @@ -14150,9 +14192,9 @@ "dev": true }, "node_modules/xstate": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.13.0.tgz", - "integrity": "sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", "peer": true, "funding": { "type": "opencollective", @@ -16792,8 +16834,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "aria-query": { "version": "5.1.3", @@ -17260,6 +17301,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -20310,7 +20356,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -20389,6 +20434,29 @@ "universalify": "^2.0.0" } }, + "jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "requires": { + "debug": "^2.1.3" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -22195,6 +22263,15 @@ "use-sync-external-store": "^1.0.0" } }, + "react-share": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-share/-/react-share-5.1.0.tgz", + "integrity": "sha512-OvyfMtj/0UzH1wi90OdHhZVJ6WUC/+IeWvBwppeZozwIGyAjQgyR0QXlHOrxVHVECqnGvcpBaFTXVrqouTieaw==", + "requires": { + "classnames": "^2.3.2", + "jsonp": "^0.2.1" + } + }, "react-sortable-tree-patch-react-17": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-sortable-tree-patch-react-17/-/react-sortable-tree-patch-react-17-2.9.0.tgz", @@ -23945,9 +24022,9 @@ "dev": true }, "xstate": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.13.0.tgz", - "integrity": "sha512-Z0om784N5u8sAzUvQJBa32jiTCIGGF/2ZsmKkerQEqeeUktAeOMK20FIHFUMywC4GcAkNksSvaeX7lwoRNXPEQ==", + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", "peer": true }, "y18n": { diff --git a/package.json b/package.json index 5d21feaac..04cee394a 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "mui-datatables": "*", "react": ">=17", "react-dom": ">=17", - "xstate": "^5.13.0" + "xstate": "^5.18.2" }, "peerDependenciesMeta": { "lodash": { @@ -116,6 +116,8 @@ "access": "public" }, "dependencies": { - "lodash": "^4.17.21" + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "react-share": "^5.1.0" } } diff --git a/src/base/Skeleton/Skeleton.tsx b/src/base/Skeleton/Skeleton.tsx new file mode 100644 index 000000000..da7150074 --- /dev/null +++ b/src/base/Skeleton/Skeleton.tsx @@ -0,0 +1,7 @@ +import { Skeleton as MuiSkeleton, type SkeletonProps as MuiSkeletonProps } from '@mui/material'; + +export function Skeleton(props: MuiSkeletonProps): JSX.Element { + return ; +} + +export default Skeleton; diff --git a/src/base/Skeleton/index.tsx b/src/base/Skeleton/index.tsx new file mode 100644 index 000000000..190aa18c9 --- /dev/null +++ b/src/base/Skeleton/index.tsx @@ -0,0 +1,5 @@ +import { SkeletonProps } from '@mui/material'; +import Skeleton from './Skeleton'; + +export { Skeleton }; +export type { SkeletonProps }; diff --git a/src/base/index.tsx b/src/base/index.tsx index 0f7d8850f..e2e44121f 100644 --- a/src/base/index.tsx +++ b/src/base/index.tsx @@ -55,6 +55,7 @@ export * from './Paper'; export * from './Popper'; export * from './RadioGroup'; export * from './Select'; +export * from './Skeleton'; export * from './Slide'; export * from './Stack'; export * from './Switch'; diff --git a/src/custom/CatalogCard/CatalogCard.tsx b/src/custom/CatalogCard/CatalogCard.tsx index 16622404a..a038e925f 100644 --- a/src/custom/CatalogCard/CatalogCard.tsx +++ b/src/custom/CatalogCard/CatalogCard.tsx @@ -33,7 +33,6 @@ type CatalogCardProps = { // eslint-disable-next-line @typescript-eslint/no-explicit-any pattern: any; patternType: string; - cardLink: string; cardHeight: string; cardWidth: string; cardStyles: React.CSSProperties; @@ -64,7 +63,6 @@ const CatalogCard: React.FC = ({ cardHeight, cardWidth, cardStyles, - cardLink, onCardClick }) => { const outerStyles = { @@ -73,49 +71,47 @@ const CatalogCard: React.FC = ({ ...cardStyles }; return ( - - - - - {patternType} - - - {pattern.name} - - - - - - - - - {pattern.download_count} - - - - {pattern.clone_count} - - - - {pattern.view_count} - - - - {pattern.deployment_count} - - - - {pattern.share_count} - - - - - + + + + {patternType} + + + {pattern.name} + + + + + + + + + {pattern.download_count} + + + + {pattern.clone_count} + + + + {pattern.view_count} + + + + {pattern.deployment_count} + + + + {pattern.share_count} + + + + ); }; diff --git a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx new file mode 100644 index 000000000..8808b42dd --- /dev/null +++ b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import _ from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import { PublishIcon } from '../../icons'; +import { CHARCOAL, useTheme } from '../../theme'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { useWindowDimensions } from '../Helpers/Dimension'; +import PromptComponent from '../Prompt'; +import { PromptRef } from '../Prompt/promt-component'; +import ResponsiveDataTable from '../ResponsiveDataTable'; +import UnpublishTooltipIcon from './UnpublishTooltipIcon'; + +interface CatalogDesignsTableProps { + patterns: Pattern[]; + filter: any; + columns: Array; + totalCount: number; + sortOrder: string; + setSortOrder: (order: string) => void; + pageSize: number; + setPageSize: (size: number) => void; + page: number; + setPage: (page: number) => void; + columnVisibility: Record; + colViews: Record | undefined; + handleBulkDeleteModal: (patterns: Pattern[], modalRef: React.RefObject) => void; + handleBulkpatternsDataUnpublishModal: ( + selected: any, + patterns: Pattern[], + modalRef: React.RefObject + ) => void; +} + +export const CatalogDesignsTable: React.FC = ({ + patterns, + filter, + columns = [], + totalCount = 0, + sortOrder = '', + setSortOrder, + pageSize = 10, + setPageSize, + page = 0, + setPage, + columnVisibility = {}, + colViews = {}, + handleBulkDeleteModal, + handleBulkpatternsDataUnpublishModal +}) => { + const [tableCols, updateCols] = useState>([]); + const { width } = useWindowDimensions(); + const smallScreen = width <= 360; + const theme = useTheme(); + const modalRef = useRef(null); + + useEffect(() => { + if (Array.isArray(columns) && columns.length > 0) { + updateCols(columns); + } + }, [columns]); + + const options: any = { + selectableRows: _.isNil(filter) ? 'none' : 'multiple', + serverSide: true, + filterType: 'multiselect', + responsive: smallScreen ? 'vertical' : 'standard', + count: totalCount, + rowsPerPage: pageSize, + page, + elevation: 0, + onTableChange: (action: string, tableState: any) => { + const sortInfo = tableState.announceText ? tableState.announceText.split(' : ') : []; + let order = ''; + if (tableState.activeColumn) { + order = `${columns[tableState.activeColumn].name} desc`; + } + switch (action) { + case 'changePage': + setPage(tableState.page); + break; + case 'changeRowsPerPage': + setPageSize(tableState.rowsPerPage); + break; + case 'sort': + if ( + sortInfo.length === 2 && + tableState.activeColumn !== undefined && + Array.isArray(columns) + ) { + if (sortInfo[1] === 'ascending') { + order = `${columns[tableState.activeColumn].name} asc`; + } else { + order = `${columns[tableState.activeColumn].name} desc`; + } + } + if (order !== sortOrder) { + setSortOrder(order); + } + break; + } + } + }; + + if (_.isNil(filter)) { + options.customToolbarSelect = (selected: any) => ( + handleBulkpatternsDataUnpublishModal(selected, patterns, modalRef)} + iconType="publish" + id={'unpublish-button'} + > + + + ); + } else { + options.onRowsDelete = (rowsDeleted: any) => { + const selectedPatterns = rowsDeleted.data.map(({ dataIndex }: any) => patterns[dataIndex]); + handleBulkDeleteModal(selectedPatterns, modalRef); + return false; + }; + } + + if (!Array.isArray(tableCols) || tableCols.length === 0) { + return null; + } + + return ( + <> + + + > + ); +}; + +export default CatalogDesignsTable; diff --git a/src/custom/CatalogDesignTable/TableVisibilityControl.tsx b/src/custom/CatalogDesignTable/TableVisibilityControl.tsx new file mode 100644 index 000000000..a153aa0cb --- /dev/null +++ b/src/custom/CatalogDesignTable/TableVisibilityControl.tsx @@ -0,0 +1,41 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; +import { CustomColumn } from '../CustomColumnVisibilityControl/CustomColumnVisibilityControl'; +import { ViewSwitch } from './ViewSwitch'; + +type TypeView = 'grid' | 'table'; + +interface TableVisibilityControlProps { + viewType: TypeView; + setViewType: (view: TypeView) => void; + filteredColumns: CustomColumn[]; + columnVisibility: Record; + setColumnVisibility: Dispatch>>; + viewSwitchDisabled?: boolean; +} + +export const TableVisibilityControl: React.FC = ({ + viewType, + setViewType, + filteredColumns, + columnVisibility, + setColumnVisibility, + viewSwitchDisabled = false +}) => { + return ( + + {viewType !== 'grid' && ( + + )} + + + ); +}; diff --git a/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx b/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx new file mode 100644 index 000000000..69b9deb21 --- /dev/null +++ b/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx @@ -0,0 +1,53 @@ +import { ReactNode } from 'react'; +import { IconButton } from '../../base'; +import { useTheme } from '../../theme'; +import { HOVER_DELETE } from '../../theme/colors/colors'; +import { CustomTooltip } from '../CustomTooltip'; +import { IconWrapper } from '../ResponsiveDataTable'; + +interface UnpublishTooltipIconProps { + children: ReactNode; + onClick: () => void; + title: string; + iconType: 'delete' | 'publish'; + id: string; + style?: object; + placement?: 'bottom' | 'top' | 'left' | 'right'; + disabled?: boolean; +} + +function UnpublishTooltipIcon({ + children, + onClick, + title, + iconType, + id, + style, + placement, + disabled = false +}: UnpublishTooltipIconProps) { + const theme = useTheme(); + return ( + + + + {children} + + + + ); +} + +export default UnpublishTooltipIcon; diff --git a/src/custom/CatalogDesignTable/ViewSwitch.tsx b/src/custom/CatalogDesignTable/ViewSwitch.tsx new file mode 100644 index 000000000..7ac8a1a23 --- /dev/null +++ b/src/custom/CatalogDesignTable/ViewSwitch.tsx @@ -0,0 +1,66 @@ +/** + * Renders a switch component for toggling between grid and table view. + * + * @typedef {("grid" | "table")} TypeView + * @typedef {object} Props + * @prop {TypeView} props.view - The current view type ("grid" or "table"). + * @prop {Function} props.changeView - The function to change the view type. + */ + +import { IconButton } from '@mui/material'; +import { GridViewIcon, TableViewIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { CustomTooltip } from '../CustomTooltip'; + +type TypeView = 'grid' | 'table'; + +interface ViewSwitchProps { + view: TypeView; + changeView: (view: TypeView) => void; + height?: string; + style?: React.CSSProperties; + disabled?: boolean; +} + +export const ViewSwitch: React.FC = ({ + view, + changeView, + height = '3rem', + style = {}, + disabled = false +}) => { + const handleClick = () => { + changeView(view === 'grid' ? 'table' : 'grid'); + }; + + const Icon = view === 'grid' ? TableViewIcon : GridViewIcon; + const label = view === 'grid' ? 'Table View' : 'Grid View'; + const theme = useTheme(); + + return ( + + + + + + + + ); +}; diff --git a/src/custom/CatalogDesignTable/columnConfig.tsx b/src/custom/CatalogDesignTable/columnConfig.tsx new file mode 100644 index 000000000..ed7260c79 --- /dev/null +++ b/src/custom/CatalogDesignTable/columnConfig.tsx @@ -0,0 +1,372 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { MUIDataTableColumn, MUIDataTableMeta } from 'mui-datatables'; +import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from 'react-share'; +import { Avatar, Box, Grid, Typography } from '../../base'; +import { iconMedium } from '../../constants/iconsSizes'; +import { + ChainIcon, + CopyIcon, + DownloadIcon, + FacebookIcon, + KanvasIcon, + LinkedinIcon, + PersonIcon, + PublishIcon, + TwitterIcon +} from '../../icons'; +import { downloadFilter, downloadYaml } from '../CatalogDetail/helper'; +import { RESOURCE_TYPES } from '../CatalogDetail/types'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { CustomTooltip } from '../CustomTooltip'; +import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; +import { DataTableEllipsisMenu } from '../ResponsiveDataTable'; +import { NameDiv } from './style'; + +export type ColView = [string, 'na' | 'xs' | 'l']; + +export const colViews: ColView[] = [ + ['id', 'na'], + ['name', 'xs'], + ['first_name', 'xs'], + ['last_name', 'na'], + ['created_at', 'na'], + ['updated_at', 'l'], + ['design_type', 'xs'], + ['class', 'l'], + ['view_count', 'na'], + ['download_count', 'na'], + ['clone_count', 'na'], + ['deployment_count', 'na'], + ['share_count', 'na'], + ['actions', 'xs'] +]; + +interface ColumnConfigProps { + handleShowDetails: (design: Pattern) => void; + handleClone: (designId: string, name: string) => void; + handleCopyUrl: (designId: string, name: string) => void; + handleOpenPlayground: (designId: string, name: string) => void; + handleUnpublish?: (design: Pattern) => void; + maxWidth?: boolean; + getCatalogUrl: (type: string, name: string) => string; + type?: string; + theme?: any; + showUnpublish?: boolean; + currentUserId?: string; + isCloneDisabled?: boolean; + isUnpublishDisabled?: boolean; +} + +interface ActionItem { + title: string; + icon?: JSX.Element; + onClick?: () => void; + disabled?: boolean; + customComponent?: JSX.Element; + type?: string; +} + +export const createDesignColumns = ({ + handleShowDetails, + handleClone, + handleCopyUrl, + handleOpenPlayground, + handleUnpublish = () => {}, + maxWidth = true, + getCatalogUrl, + type, + theme, + showUnpublish, + currentUserId, + isCloneDisabled, + isUnpublishDisabled +}: ColumnConfigProps): MUIDataTableColumn[] => { + const cleanedType = type?.replace('my-', '').replace(/s$/, ''); + const getColumnValue = (tableMeta: MUIDataTableMeta, targetColumn: string): any => { + //@ts-ignore + const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + return (rowData as any)[targetColumn] || ''; + }; + + return [ + { + name: 'id', + label: 'ID', + options: { + filter: false, + customBodyRender: (value: string) => + } + }, + { + name: 'name', + label: 'Name', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (value: string, tableMeta: MUIDataTableMeta) => { + //@ts-ignore + const design = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + return handleShowDetails(design)}>{value}; + } + } + }, + { + name: 'avatar_url', + label: 'Avatar', + options: { + display: false + } + }, + { + name: 'user_id', + label: 'User ID', + options: { + display: false + } + }, + { + name: 'first_name', + label: 'Author', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (_: string, tableMeta: MUIDataTableMeta) => { + const firstName = getColumnValue(tableMeta, 'first_name'); + const lastName = getColumnValue(tableMeta, 'last_name'); + const avatar_url = getColumnValue(tableMeta, 'avatar_url'); + const user_id = getColumnValue(tableMeta, 'user_id'); + const displayName = + firstName && lastName + ? `${firstName} ${lastName}` + : firstName + ? firstName + : lastName + ? lastName + : ''; + + return ( + img': { mr: 2, flexShrink: 0 } }}> + + + + + + { + window.location.href = `/user/${user_id}`; + }} + > + {!avatar_url && } + + + + + + {maxWidth && ( + + {displayName} + + )} + + + ); + } + } + }, + { + name: 'last_name', + label: 'Last Name', + options: { + display: false + } + }, + { + name: 'created_at', + label: 'Created At', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'updated_at', + label: 'Updated At', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'design_type', + label: 'Type', + options: { + filter: true, + sort: false, + searchable: true + } + }, + { + name: 'class', + label: 'Class', + options: { + filter: true, + sort: false, + searchable: true + } + }, + { + name: 'view_count', + label: 'Opens', + options: { + filter: false, + sort: true + } + }, + { + name: 'download_count', + label: 'Downloads', + options: { + filter: false, + sort: true + } + }, + { + name: 'clone_count', + label: 'Clones', + options: { + filter: false, + sort: true + } + }, + { + name: 'deployment_count', + label: 'Deploys', + options: { + filter: false, + sort: true + } + }, + { + name: 'share_count', + label: 'Shares', + options: { + filter: false, + sort: true + } + }, + { + name: 'actions', + label: 'Actions', + options: { + filter: false, + sort: false, + searchable: false, + setCellHeaderProps: () => ({ align: 'center' }), + setCellProps: () => ({ align: 'center' }), + customBodyRender: (_: any, tableMeta: MUIDataTableMeta) => { + //@ts-ignore + const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + + function constructMessage() { + const currentUser = rowData?.user_id === currentUserId; + if (currentUser) { + return `Check out my design "${rowData?.name}" on Layer5's Catalog`; + } else { + return `Check out ${ + rowData?.first_name + ' ' + rowData.last_name + }'s design "${rowData?.name}" on Layer5's Catalog`; + } + } + const baseActions: ActionItem[] = [ + { + title: 'Clone', + onClick: () => handleClone(rowData.id, rowData.name), + disabled: isCloneDisabled, + icon: + }, + { + title: 'Download', + onClick: () => { + cleanedType === RESOURCE_TYPES.FILTERS + ? downloadFilter(rowData.id, rowData.name) + : downloadYaml(rowData.pattern_file, rowData.name); + }, + icon: + }, + { + title: 'Copy Link', + onClick: () => handleCopyUrl(rowData.id, rowData.name), + icon: + }, + { + title: 'Share Design via Socials', + type: 'share-social', + customComponent: ( + + + + + + + + + + + + ) + }, + { + title: 'Open in playground', + onClick: () => handleOpenPlayground(rowData.id, rowData.name), + icon: + } + ]; + + const actionsList = showUnpublish + ? [ + ...baseActions.slice(0, 2), + { + title: 'Unpublish', + onClick: () => handleUnpublish(rowData), + disabled: isUnpublishDisabled, + icon: + }, + ...baseActions.slice(2) + ] + : baseActions; + + //@ts-ignore + return ; + } + } + } + ]; +}; diff --git a/src/custom/CatalogDesignTable/index.ts b/src/custom/CatalogDesignTable/index.ts new file mode 100644 index 000000000..fcab02087 --- /dev/null +++ b/src/custom/CatalogDesignTable/index.ts @@ -0,0 +1,5 @@ +import CatalogDesignsTable from './CatalogDesignTable'; +import { colViews, createDesignColumns } from './columnConfig'; +export { TableVisibilityControl } from './TableVisibilityControl'; +export { ViewSwitch } from './ViewSwitch'; +export { CatalogDesignsTable, colViews, createDesignColumns }; diff --git a/src/custom/CatalogDesignTable/style.tsx b/src/custom/CatalogDesignTable/style.tsx new file mode 100644 index 000000000..502129fe7 --- /dev/null +++ b/src/custom/CatalogDesignTable/style.tsx @@ -0,0 +1,11 @@ +import { styled } from '@mui/material'; + +export const NameDiv = styled('div')({ + cursor: 'pointer', + fontWeight: 'bold', + textDecoration: 'none', + + '&:hover': { + textDecoration: 'underline' + } +}); diff --git a/src/custom/CatalogDetail/ActionButton.tsx b/src/custom/CatalogDetail/ActionButton.tsx new file mode 100644 index 000000000..41154b03e --- /dev/null +++ b/src/custom/CatalogDetail/ActionButton.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { CircularProgress } from '../../base'; +import { CopyIcon, KanvasIcon, PublishIcon } from '../../icons'; +import Download from '../../icons/Download/Download'; +import { charcoal, useTheme } from '../../theme'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { downloadFilter, downloadYaml } from './helper'; +import { ActionButton, StyledActionWrapper, UnpublishAction } from './style'; +import { RESOURCE_TYPES } from './types'; + +interface ActionButtonsProps { + actionItems: boolean; + details: Pattern; + type: string; + isCloneLoading: boolean; + handleClone: (name: string, id: string) => void; + handleUnpublish: () => void; + isCloneDisabled: boolean; + showUnpublishAction: boolean; + onOpenPlaygroundClick: (designId: string, name: string) => void; +} + +const ActionButtons: React.FC = ({ + actionItems, + details, + type, + isCloneLoading, + handleClone, + isCloneDisabled, + showUnpublishAction, + handleUnpublish, + onOpenPlaygroundClick +}) => { + const cleanedType = type.replace('my-', '').replace(/s$/, ''); + const theme = useTheme(); + return ( + + {actionItems && ( + + + cleanedType === RESOURCE_TYPES.FILTERS + ? downloadFilter(details.id, details.name) + : downloadYaml(details.pattern_file, details.name) + } + > + + Download + + + {cleanedType !== RESOURCE_TYPES.FILTERS && ( + handleClone(details?.name, details?.id)} + disabled={isCloneDisabled} + > + {isCloneLoading ? ( + + ) : ( + <> + + Clone + > + )} + + )} + + )} + + onOpenPlaygroundClick(details.id, details.name)} + > + + Open in Playground + + + {showUnpublishAction && ( + + + Unpublish + + )} + + ); +}; + +export default ActionButtons; diff --git a/src/custom/CatalogDetail/CaveatsSection.tsx b/src/custom/CatalogDetail/CaveatsSection.tsx new file mode 100644 index 000000000..489be4133 --- /dev/null +++ b/src/custom/CatalogDetail/CaveatsSection.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { RenderMarkdown } from '../Markdown'; +import { ContentDetailsText } from '../Typography'; +import { CaveatsContainer, ContentHeading } from './style'; + +interface CaveatsSectionProps { + details: Pattern; +} + +const CaveatsSection: React.FC = ({ details }) => { + return ( + + + CAVEATS AND CONSIDERATIONS + + {details?.catalog_data?.pattern_caveats ? ( + + + + ) : ( + No caveats registered + )} + + ); +}; + +export default CaveatsSection; diff --git a/src/custom/CatalogDetail/ChallengesSection.tsx b/src/custom/CatalogDetail/ChallengesSection.tsx new file mode 100644 index 000000000..77028a2f0 --- /dev/null +++ b/src/custom/CatalogDetail/ChallengesSection.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react'; +import { Link, ListItemIcon } from '../../base'; +import { ChallengesIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import CollapsibleSection from './CollapsibleSection'; +import { slugify } from './helper'; +import { LabelDiv } from './style'; +import { FilteredAcademyData } from './types'; + +interface ChallengesSectionProps { + filteredAcademyData: FilteredAcademyData; +} + +const ChallengesSection: React.FC = ({ filteredAcademyData }) => { + const theme = useTheme(); + const [openChallenges, setOpenChallenges] = useState(false); + const [autoUpdate, setAutoUpdate] = useState(true); + + useEffect(() => { + if (autoUpdate) { + setOpenChallenges((filteredAcademyData?.['challenges'] ?? []).length > 0); + } + }, [filteredAcademyData, autoUpdate]); + + const toggleOpenChallenges = () => { + setOpenChallenges((prev) => !prev); + setAutoUpdate(false); + }; + + const renderChallengeItem = (item: string, index: number) => ( + + + + + + {item} + + + ); + + return ( + <> + + + > + ); +}; + +export default ChallengesSection; diff --git a/src/custom/CatalogDetail/CollapsibleSection.tsx b/src/custom/CatalogDetail/CollapsibleSection.tsx new file mode 100644 index 000000000..49c417947 --- /dev/null +++ b/src/custom/CatalogDetail/CollapsibleSection.tsx @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { Collapse, List, ListItemText } from '../../base'; +import { InfoTooltip } from '../CustomTooltip'; +import { SideContainer, SideTitleButton } from './style'; + +interface CollapsibleSectionProps { + title: string; + isOpen: boolean; + onToggle: () => void; + items: any[]; + renderItem: (item: any, index: number) => React.ReactNode; + emptyState: string; + tooltip: string; +} + +const CollapsibleSection: React.FC = ({ + title, + isOpen, + onToggle, + items, + renderItem, + emptyState, + tooltip +}) => { + return ( + + + + + {isOpen ? : } + + + {items && items.length > 0 ? ( + + {items?.map(renderItem)} + + ) : ( + + )} + + + ); +}; + +export default CollapsibleSection; diff --git a/src/custom/CatalogDetail/ContentClassInfo.tsx b/src/custom/CatalogDetail/ContentClassInfo.tsx new file mode 100644 index 000000000..effd24378 --- /dev/null +++ b/src/custom/CatalogDetail/ContentClassInfo.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Box } from '../../base'; +import { CommunityClassIcon, OfficialClassIcon, VerificationClassIcon } from '../../icons'; +import { KEPPEL, useTheme } from '../../theme'; +import { InfoTooltip } from '../CustomTooltip'; +import { ContentDetailsPoints, ContentDetailsText } from '../Typography'; +import { formatToTitleCase } from './helper'; +import { Class, ContentClassType } from './types'; + +interface ContentClassInfoProps { + contentClass: string; + classes: Class[]; +} + +const ContentClassInfo: React.FC = ({ contentClass, classes }) => { + const _classDescription = (className: string): string | undefined => { + const classObj = classes && classes.find((classObj) => classObj.class === className); + return classObj?.description; + }; + + const theme = useTheme(); + + const CONTENT_CLASS: ContentClassType = { + community: { + icon: CommunityClassIcon, + color: theme.palette.icon.secondary + }, + official: { + icon: OfficialClassIcon, + color: '#EBC017' + }, + verified: { + icon: VerificationClassIcon, + color: theme.palette.primary.brand?.default || KEPPEL + } + } as const; + + const ClassIcon: React.FC<{ className: string }> = ({ className }) => { + const Icon = CONTENT_CLASS[className]?.icon; + const fill = CONTENT_CLASS[className]?.color; + return Icon ? : null; + }; + + return ( + + + + CLASS + + + + + + {formatToTitleCase(contentClass)} + + + ); +}; + +export default ContentClassInfo; diff --git a/src/custom/CatalogDetail/LearningSection.tsx b/src/custom/CatalogDetail/LearningSection.tsx new file mode 100644 index 000000000..d6e308075 --- /dev/null +++ b/src/custom/CatalogDetail/LearningSection.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { Link, ListItemIcon } from '../../base'; +import { LearningIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import CollapsibleSection from './CollapsibleSection'; +import { slugify } from './helper'; +import { LabelDiv } from './style'; +import { FilteredAcademyData } from './types'; + +interface LearningSectionProps { + filteredAcademyData: FilteredAcademyData; +} + +const LearningSection: React.FC = ({ filteredAcademyData }) => { + const theme = useTheme(); + const [openLearning, setOpenLearning] = useState(false); + const [autoUpdate, setAutoUpdate] = useState(true); + + useEffect(() => { + if (autoUpdate) { + setOpenLearning(Boolean((filteredAcademyData?.['learning-path'] ?? []).length > 0)); + } + }, [filteredAcademyData, autoUpdate]); + + const toggleOpenLearning = (): void => { + setOpenLearning((prev) => !prev); + setAutoUpdate(false); + }; + + const renderLearningItem = (item: string, index: number) => ( + + + + + + {item} + + + ); + + return ( + <> + + + > + ); +}; + +export default LearningSection; diff --git a/src/custom/CatalogDetail/LeftPanel.tsx b/src/custom/CatalogDetail/LeftPanel.tsx new file mode 100644 index 000000000..a707651ab --- /dev/null +++ b/src/custom/CatalogDetail/LeftPanel.tsx @@ -0,0 +1,97 @@ +import { useTheme } from '../../theme'; +import { CatalogCardDesignLogo, CustomCatalogCard } from '../CustomCatalog'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import ActionButtons from './ActionButton'; +import ChallengesSection from './ChallengesSection'; +import LearningSection from './LearningSection'; +import TechnologySection from './TechnologySection'; +import { FilteredAcademyData } from './types'; + +interface LeftPanelProps { + details: Pattern; + type: string; + actionItems?: boolean; + isCloneLoading: boolean; + handleClone: (name: string, id: string) => void; + showTechnologies?: boolean; + filteredAcademyData: FilteredAcademyData; + isCloneDisabled: boolean; + technologySVGPath: string; + technologySVGSubpath: string; + fontFamily?: string; + handleUnpublish: () => void; + showUnpublishAction?: boolean; + onOpenPlaygroundClick: (designId: string, name: string) => void; +} + +const LeftPanel: React.FC = ({ + details, + type, + actionItems = true, + isCloneLoading, + handleClone, + handleUnpublish, + showTechnologies = true, + filteredAcademyData, + isCloneDisabled, + technologySVGPath, + technologySVGSubpath, + fontFamily, + showUnpublishAction = false, + onOpenPlaygroundClick +}) => { + const theme = useTheme(); + + return ( + + + + + + {showTechnologies && ( + + )} + + + + ); +}; + +export default LeftPanel; diff --git a/src/custom/CatalogDetail/MetricsDisplay.tsx b/src/custom/CatalogDetail/MetricsDisplay.tsx new file mode 100644 index 000000000..85cf7ec42 --- /dev/null +++ b/src/custom/CatalogDetail/MetricsDisplay.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Grid } from '../../base'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { MetricsContainer, MetricsData, MetricsSection, MetricsType } from './style'; + +interface MetricItem { + label: string; + value: number; +} + +interface MetricsDisplayProps { + details: Pattern; +} + +const MetricsDisplay: React.FC = ({ details }) => { + const metrics: MetricItem[] = [ + { label: 'Opens', value: details.view_count }, + { label: 'Downloads', value: details.download_count }, + { label: 'Deploys', value: details.deployment_count }, + { label: 'Clones', value: details.clone_count }, + { label: 'Shares', value: details.share_count } + ]; + + return ( + + + {metrics.map((metric) => ( + + {metric.value} + {metric.label} + + ))} + + + ); +}; + +export default MetricsDisplay; diff --git a/src/custom/CatalogDetail/OverviewSection.tsx b/src/custom/CatalogDetail/OverviewSection.tsx new file mode 100644 index 000000000..4d842bde7 --- /dev/null +++ b/src/custom/CatalogDetail/OverviewSection.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { Grid } from '../../base'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import ContentClassInfo from './ContentClassInfo'; +import MetricsDisplay from './MetricsDisplay'; +import PatternInfo from './PatternInfo'; +import SocialSharePopper from './SocialSharePopper'; +import UserInfo from './UserInfo'; +import { ContentRow, DesignHeading, OverviewContainer } from './style'; +import { Class } from './types'; + +interface OverviewSectionProps { + details: Pattern; + type: string; + cardId: string; + title: string; + getUrl: (type: string, id: string) => string; + showContentDetails: boolean; + ViewsComponent?: React.ReactNode; + showVersion: boolean; + classes: Class[]; + handleCopyUrl: (type: string, name: string, id: string) => void; + fontFamily?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + userProfile?: any; +} + +const OverviewSection: React.FC = ({ + details, + type, + cardId, + title, + getUrl, + showContentDetails, + ViewsComponent, + showVersion, + classes, + handleCopyUrl, + fontFamily, + userProfile +}) => { + return ( + + + {details?.name} + + + + + + + {details?.catalog_data?.content_class && ( + + + + )} + + + + + + {showContentDetails ? ( + + WHAT DOES THIS DESIGN DO? + {details?.catalog_data?.pattern_info ? ( + + ) : ( + No description available + )} + + ) : ( + ViewsComponent + )} + + {!(type === 'view' || type === 'filter') && } + + + ); +}; + +export default OverviewSection; diff --git a/src/custom/CatalogDetail/PatternInfo.tsx b/src/custom/CatalogDetail/PatternInfo.tsx new file mode 100644 index 000000000..6371ddcd7 --- /dev/null +++ b/src/custom/CatalogDetail/PatternInfo.tsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import { RenderMarkdown } from '../Markdown'; +import { ShowToggleBtn } from './style'; + +interface PatternInfoProps { + text: string; + redirect?: boolean; + id?: string; +} + +const PatternInfo: React.FC = ({ text, redirect, id }) => { + const [isExpanded, setIsExpanded] = useState(false); + + const toggleExpand = () => { + setIsExpanded(!isExpanded); + }; + + const handleRedirect = () => { + if (id) { + window.location.href = `/catalog/content/design/${id}`; + } + }; + + return ( + + {isExpanded ? ( + + + show less + + ) : ( + + + {text.length > (redirect ? 400 : 500) && ( + + ...show more + + )} + + )} + + ); +}; + +export default PatternInfo; diff --git a/src/custom/CatalogDetail/RelatedDesigns.tsx b/src/custom/CatalogDetail/RelatedDesigns.tsx new file mode 100644 index 000000000..9449f21f9 --- /dev/null +++ b/src/custom/CatalogDetail/RelatedDesigns.tsx @@ -0,0 +1,75 @@ +import { CatalogCardDesignLogo } from '../CustomCatalog'; +import CustomCatalogCard, { Pattern } from '../CustomCatalog/CustomCard'; +import { formatToTitleCase } from './helper'; +import { AdditionalContainer, ContentHeading, DesignCardContainer } from './style'; +import { UserProfile } from './types'; + +export interface PatternsPerUser { + patterns: Pattern[]; +} + +interface RelatedDesignsProps { + details: Pattern; + type: string; + patternsPerUser: PatternsPerUser; + onSuggestedPatternClick: (pattern: Pattern) => void; + userProfile?: UserProfile; + technologySVGPath: string; + technologySVGSubpath: string; + orgName: string; + fetchingOrgError: boolean; +} + +const RelatedDesigns: React.FC = ({ + details, + type, + patternsPerUser, + onSuggestedPatternClick, + userProfile, + technologySVGPath, + technologySVGSubpath, + orgName, + fetchingOrgError +}) => { + const filteredPatternsPerUser = patternsPerUser?.patterns?.filter( + (pattern) => pattern.id !== details.id + ); + + if (!filteredPatternsPerUser?.length) return null; + + return ( + + + + Other published design by {formatToTitleCase(userProfile?.first_name ?? '')}{' '} + {fetchingOrgError ? '' : `under ${orgName}`} + + + + {filteredPatternsPerUser.map((pattern, index) => ( + onSuggestedPatternClick(pattern)} + UserName={`${userProfile?.first_name ?? ''} ${userProfile?.last_name ?? ''}`} + avatarUrl={userProfile?.avatar_url} + basePath={technologySVGPath} + subBasePath={technologySVGSubpath} + cardTechnologies={true} + > + + + ))} + + + ); +}; + +export default RelatedDesigns; diff --git a/src/custom/CatalogDetail/RightPanel.tsx b/src/custom/CatalogDetail/RightPanel.tsx new file mode 100644 index 000000000..24d52f070 --- /dev/null +++ b/src/custom/CatalogDetail/RightPanel.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import CaveatsSection from './CaveatsSection'; +import OverviewSection from './OverviewSection'; +import RelatedDesigns, { PatternsPerUser } from './RelatedDesigns'; +import { Class } from './types'; + +interface RightPanelProps { + details: Pattern; + type: string; + cardId?: string; + title: string; + getUrl: (type: string, id: string) => string; + showContentDetails: boolean; + ViewsComponent?: React.ReactNode; + showVersion: boolean; + showCaveats: boolean; + classes: Class[]; + patternsPerUser: PatternsPerUser; + handleCopyUrl: (type: string, name: string, id: string) => void; + onSuggestedPatternClick: (pattern: Pattern) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useGetUserProfileByIdQuery: any; + fontFamily?: string; + technologySVGPath: string; + technologySVGSubpath: string; + orgName: string; + fetchingOrgError: boolean; +} + +const RightPanel: React.FC = ({ + details, + type, + cardId = details.id, + title, + getUrl, + showContentDetails, + ViewsComponent, + showVersion, + showCaveats, + classes, + patternsPerUser, + onSuggestedPatternClick, + handleCopyUrl, + fontFamily, + useGetUserProfileByIdQuery, + technologySVGPath, + technologySVGSubpath, + orgName, + fetchingOrgError +}) => { + const cleanedType = type.replace('my-', '').replace(/s$/, ''); + const { data: userProfile } = useGetUserProfileByIdQuery({ + id: details.user_id + }); + + return ( + + + {showCaveats && } + + + ); +}; + +export default RightPanel; diff --git a/src/custom/CatalogDetail/SocialSharePopper.tsx b/src/custom/CatalogDetail/SocialSharePopper.tsx new file mode 100644 index 000000000..3c24b32d8 --- /dev/null +++ b/src/custom/CatalogDetail/SocialSharePopper.tsx @@ -0,0 +1,125 @@ +import { Box, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'; +import React, { useState } from 'react'; +import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from 'react-share'; +import { ChainIcon, FacebookIcon, LinkedinIcon, ShareIcon, TwitterIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { ErrorBoundary } from '../ErrorBoundary'; +import { CopyShareIconWrapper, VisibilityChip } from './style'; + +interface SocialSharePopperProps { + details: Pattern; + type: string; + cardId: string; + title: string; + getUrl: (type: string, id: string) => string; + handleCopyUrl: (type: string, name: string, id: string) => void; +} + +const SocialSharePopper: React.FC = ({ + details, + type, + cardId, + title, + getUrl, + handleCopyUrl +}) => { + const theme = useTheme(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const cleanedType = type.replace('my-', '').replace(/s$/, ''); + + return ( + + + + {details?.visibility} + + + {details?.visibility !== 'private' && ( + + handleCopyUrl(cleanedType, details?.name, details?.id)} + > + + + + )} + + {(details?.visibility === 'published' || details?.visibility === 'public') && ( + <> + + + + + + + + + + + + + + + + + + + + + + > + )} + + + ); +}; + +export default SocialSharePopper; diff --git a/src/custom/CatalogDetail/TechnologySection.tsx b/src/custom/CatalogDetail/TechnologySection.tsx new file mode 100644 index 000000000..4018c6255 --- /dev/null +++ b/src/custom/CatalogDetail/TechnologySection.tsx @@ -0,0 +1,78 @@ +import React, { useEffect, useState } from 'react'; +import { ListItemIcon } from '../../base'; +import { useTheme } from '../../theme'; +import CollapsibleSection from './CollapsibleSection'; +import { LabelDiv } from './style'; + +interface TechnologySectionProps { + technologies: string[]; + technologySVGPath: string; + technologySVGSubpath: string; +} + +const TechnologySection: React.FC = ({ + technologySVGPath, + technologySVGSubpath, + technologies +}) => { + const [openTechnologies, setOpenTechnologies] = useState(true); + const [validTechnologies, setValidTechnologies] = useState([]); + const theme = useTheme(); + + useEffect(() => { + // Function to check if SVG exists + const validateTechnologies = async () => { + const validTechs = await Promise.all( + technologies.map(async (tech) => { + const svg_path = `/${technologySVGPath}/${tech.toLowerCase()}/${technologySVGSubpath}/${tech.toLowerCase()}-color.svg`; + try { + const response = await fetch(svg_path); + return response.ok ? tech : null; + } catch { + return null; + } + }) + ); + setValidTechnologies(validTechs.filter((tech): tech is string => tech !== null)); + }; + + validateTechnologies(); + }, [technologies, technologySVGPath, technologySVGSubpath]); + + const renderTechnologyItem = (item: string, index: number) => { + const svg_path = `${technologySVGPath}/${item.toLowerCase()}/${technologySVGSubpath}/${item.toLowerCase()}-color.svg`; + return ( + + + + + {item} + + ); + }; + + return ( + <> + + setOpenTechnologies((prev) => !prev)} + items={validTechnologies} + renderItem={renderTechnologyItem} + emptyState={'No technologies assigned to this design'} + tooltip={'Technologies used in this design'} + /> + > + ); +}; + +export default TechnologySection; diff --git a/src/custom/CatalogDetail/UserInfo.tsx b/src/custom/CatalogDetail/UserInfo.tsx new file mode 100644 index 000000000..7165ddb22 --- /dev/null +++ b/src/custom/CatalogDetail/UserInfo.tsx @@ -0,0 +1,63 @@ +import { Avatar } from '../../base'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { getVersion } from '../CustomCatalog/Helper'; +import { formatDate } from './helper'; +import { ContentDetailsPoints, ContentDetailsText, ContentRow, RedirectLink } from './style'; +import { UserProfile } from './types'; + +interface UserInfoProps { + details: Pattern; + showVersion?: boolean; + userProfile?: UserProfile; +} + +const UserInfo: React.FC = ({ details, showVersion = true, userProfile }) => { + return ( + <> + + CREATED BY + + + + + {userProfile?.first_name} {userProfile?.last_name} + + + + + + CREATED AT + {formatDate(details?.created_at)} + + + UPDATED AT + {formatDate(details?.updated_at)} + + {showVersion && ( + + VERSION + {getVersion(details)} + + )} + > + ); +}; + +export default UserInfo; diff --git a/src/custom/CatalogDetail/helper.ts b/src/custom/CatalogDetail/helper.ts new file mode 100644 index 000000000..2d6601f77 --- /dev/null +++ b/src/custom/CatalogDetail/helper.ts @@ -0,0 +1,61 @@ +import jsyaml from 'js-yaml'; + +export const downloadYaml = (filteredData: string, itemName: string): void => { + const yamlData = Array.isArray(filteredData) + ? jsyaml.dump(filteredData.find((item) => item.name === itemName)) + : filteredData; + const blob = new Blob([yamlData], { type: 'application/yaml' }); + const url = URL.createObjectURL(blob); + const element = document.createElement('a'); + element.href = url; + element.download = `${itemName}.yaml`; + // document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + URL.revokeObjectURL(url); +}; + +export function slugify(str: string): string { + if (!str) return str; + str = str.replace(/^\s+|\s+$/g, ''); // trim leading/trailing whitespace + str = str.toLowerCase(); + + // remove accents, swap ñ for n, etc + const from = 'à áäâèéëêìÃïîòóöôùúüûñç·/_,:;'; + const to = 'aaaaeeeeiiiioooouuuunc------'; + for (let i = 0, l = from.length; i < l; i++) { + str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)); + } + + str = str + .replace(/[^a-z0-9 -]/g, '') // remove invalid chars + .replace(/\s+/g, '-') // collapse whitespace and replace by - + .replace(/-+/g, '-'); // collapse dashes + + return str; +} + +export const downloadFilter = (id: string, name: string): void => { + const dataUri = `${process.env.API_ENDPOINT_PREFIX}/api/content/filters/download/${id}`; + + // Add the .wasm extension to the filename + const fileNameWithExtension = name + '.wasm'; + + const linkElement = document.createElement('a'); + linkElement.setAttribute('href', dataUri); + linkElement.setAttribute('download', fileNameWithExtension); + linkElement.click(); + linkElement.remove(); +}; + +export const formatToTitleCase = (value: string): string => { + if (typeof value === 'string') { + return value.substring(0, 1).toUpperCase().concat('', value.substring(1).toLowerCase()); + } + return ''; +}; + +export const formatDate = (date: Date) => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const formattedDate = new Date(date).toLocaleDateString('en-US', options); + return formattedDate; +}; diff --git a/src/custom/CatalogDetail/index.tsx b/src/custom/CatalogDetail/index.tsx new file mode 100644 index 000000000..87a3d0208 --- /dev/null +++ b/src/custom/CatalogDetail/index.tsx @@ -0,0 +1,31 @@ +import ActionButtons from './ActionButton'; +import CaveatsSection from './CaveatsSection'; +import ChallengesSection from './ChallengesSection'; +import CollapsibleSection from './CollapsibleSection'; +import LearningSection from './LearningSection'; +import LeftPanel from './LeftPanel'; +import MetricsDisplay from './MetricsDisplay'; +import OverviewSection from './OverviewSection'; +import PatternInfo from './PatternInfo'; +import RelatedDesigns from './RelatedDesigns'; +import RightPanel from './RightPanel'; +import SocialSharePopper from './SocialSharePopper'; +import TechnologySection from './TechnologySection'; +import UserInfo from './UserInfo'; + +export { + ActionButtons, + CaveatsSection, + ChallengesSection, + CollapsibleSection, + LearningSection, + LeftPanel, + MetricsDisplay, + OverviewSection, + PatternInfo, + RelatedDesigns, + RightPanel, + SocialSharePopper, + TechnologySection, + UserInfo +}; diff --git a/src/custom/CatalogDetail/style.tsx b/src/custom/CatalogDetail/style.tsx new file mode 100644 index 000000000..f30972683 --- /dev/null +++ b/src/custom/CatalogDetail/style.tsx @@ -0,0 +1,268 @@ +import { Link, ListItemButton, Paper, Typography } from '../../base'; +import { styled } from '../../theme'; +import { Theme } from './types'; + +export const StyledActionWrapper = styled(Paper)(() => ({ + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)', + justifyContent: 'center', + width: '100%', + margin: '0', + marginTop: '1rem', + display: 'flex', + flexDirection: 'column', + gap: '1rem', + padding: '0.6rem', + alignItems: 'center' +})); + +interface ActionButtonProps { + disabled?: boolean; + theme?: Theme; +} + +export const ActionButton = styled('div')(({ disabled = false, theme }) => ({ + cursor: disabled ? 'not-allowed' : 'pointer', + opacity: disabled ? '0.5' : '1', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '0.5rem', + backgroundColor: theme.palette.background.brand?.default, + padding: '0.5rem', + color: theme.palette.text.inverse, + gap: '0.625rem', + flex: '1' +})); + +export const UnpublishAction = styled('div')(({ disabled = false, theme }) => ({ + cursor: disabled ? 'not-allowed' : 'pointer', + opacity: disabled ? '0.5' : '1', + textAlign: 'center', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: '0.5rem', + backgroundColor: 'transparent', + border: `1px solid ${theme.palette.border.normal}`, + padding: '0.5rem', + color: theme.palette.text.default, + gap: '0.625rem', + flex: '1' +})); + +export const ContentDetailsText = styled(Typography)(({ theme, style }) => ({ + fontFamily: 'inherit', + fontSize: '1rem', + color: theme.palette.text.default, + ['@media (min-width:1200px)']: { + fontSize: '1' + }, + ...style +})); + +export const ContentHeading = styled('div')(() => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + marginBottom: '1rem' +})); + +export const CaveatsContainer = styled('div')(({ theme }) => ({ + width: '100%', + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)', + backgroundColor: theme.palette.background.default, + textAlign: 'left', + justifyContent: 'start', + alignItems: 'start', + display: 'flex', + flexWrap: 'wrap', + flexDirection: 'column', + padding: '1.5rem', + marginTop: '1.5rem', + borderRadius: '0.4rem', + overflowWrap: 'anywhere' +})); + +interface LabelDivProps { + clickable?: boolean; +} + +export const LabelDiv = styled('div')(({ theme, clickable }) => ({ + display: 'flex', + justifyContent: 'start', + alignItems: 'center', + padding: '0.5rem 1.5rem', + width: '100%', + borderBottom: `1px solid ${theme.palette.border.default}`, + [' @media (min-width: 600px) and (max-width: 800px)']: { + padding: '0.5rem' + }, + ...(clickable && { + '&:hover': { + backgroundColor: theme.palette.background.hover + }, + cursor: 'pointer' + }) +})); + +export const SideContainer = styled('div')(({ theme }) => ({ + width: '100%', + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)', + backgroundColor: theme.palette.background.default, + justifyContent: 'start', + alignItems: 'start', + display: 'flex', + flexDirection: 'column', + padding: '0.5rem', + borderRadius: '0.4rem' +})); + +export const SideTitleButton = styled(ListItemButton)(({ theme }) => ({ + backgroundColor: theme.palette.background.surfaces, + borderRadius: '0.5rem', + marginTop: 2, + width: '100%', + [' @media (min-width: 600px) and (max-width: 800px)']: { + padding: '0.5rem' + } +})); + +export const ContentDetailsPoints = styled(Typography)(() => ({ + fontSize: '.9rem', + fontWeight: 'bold', + lineHeight: '1.5rem', + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: '1rem', + fontFamily: 'inherit' +})); + +export const MetricsSection = styled('div')(() => ({ + padding: '1.1rem', + marginTop: '0.5rem', + display: 'flex', + borderTop: '0.5px solid #3C494F', + justifyContent: 'center', + gap: '1.7rem', + flexWrap: 'wrap', + ['@media (max-width:1200px)']: { + justifyContent: 'flex-start' + } +})); + +export const MetricsContainer = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + [' @media (min-width: 280px) and (max-width: 700px)']: { + flexBasis: '35%' + }, + [' @media (max-width: 280px)']: { + flexBasis: '10%' + } +})); +export const MetricsType = styled('div')(({ theme }) => ({ + display: 'flex', + fontSize: '16px', + fontWeight: '400', + letterSpacing: '0.15px', + lineHeight: '1.5', + textTransform: 'lowercase', + color: theme.palette.background.supplementary, + [' @media (max-width: 285px)']: { + fontSize: '0.86rem' + } +})); +export const MetricsData = styled('div')(({ theme }) => ({ + color: theme.palette.background.supplementary, + fontSize: '1.2rem', + fontWeight: 'bold', + lineHeight: '1.5' +})); + +export const OverviewContainer = styled('div')(({ theme }) => ({ + width: '100%', + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)', + backgroundColor: theme.palette.background.default, + textAlign: 'left', + justifyContent: 'start', + alignItems: 'start', + display: 'flex', + flexWrap: 'wrap', + flexDirection: 'column', + padding: '1.5rem', + borderRadius: '0.4rem' +})); + +export const DesignHeading = styled('h1')(({ theme }) => ({ + textAlign: 'left', + margin: '0rem 0rem 2rem 0rem', + color: theme.palette.text.default, + textTransform: 'capitalize', + fontWeight: '300', + flex: '1' +})); + +export const ContentRow = styled('div')(() => ({ + padding: '0.5rem 0', + overflowWrap: 'anywhere', + fontFamily: 'inherit' +})); + +export const ShowToggleBtn = styled('span')(({ theme }) => ({ + color: theme.palette.background.brand?.default, + cursor: 'pointer', + fontSize: '1rem', + fontWeight: 'normal', + marginLeft: '0.25rem' +})); + +export const AdditionalContainer = styled('div')(({ theme }) => ({ + width: '100%', + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)', + backgroundColor: theme.palette.background.default, + textAlign: 'left', + justifyContent: 'center', + alignItems: 'center', + display: 'flex', + flexWrap: 'wrap', + flexDirection: 'column', + padding: '1.5rem', + paddingBottom: '2rem', + marginTop: '1.5rem', + borderRadius: '0.4rem' +})); + +export const DesignCardContainer = styled('div')(() => ({ + display: 'flex', + flexWrap: 'wrap', + flex: '0 0 75%', + gap: '2rem', + justifyContent: 'space-around', + height: 'fit-content' +})); + +export const CopyShareIconWrapper = styled(ContentHeading)(() => ({ + justifyContent: 'flex-end', + gap: '1rem', + width: 'fit-content' +})); + +export const VisibilityChip = styled('div')(() => ({ + borderRadius: '0.5rem', + border: '1px solid gray', + padding: '0.2rem 0.5rem', + textTransform: 'capitalize', + color: '#1a1a1acc', + width: 'fit-content' +})); + +export const RedirectLink = styled(Link)(({ theme }) => ({ + color: theme.palette.background.brand?.default, + textDecoration: 'none', + cursor: 'pointer' +})); diff --git a/src/custom/CatalogDetail/types.ts b/src/custom/CatalogDetail/types.ts new file mode 100644 index 000000000..1cd2adc78 --- /dev/null +++ b/src/custom/CatalogDetail/types.ts @@ -0,0 +1,59 @@ +export interface User { + id: string; + first_name: string; + last_name: string; +} + +export interface FilteredAcademyData { + 'learning-path'?: string[]; + challenge?: string[]; + challenges?: string[]; +} + +export interface Class { + class: string; + description: string; +} + +export interface Theme { + palette: { + background: { + secondary: string; + inverse: string; + cta: { + default: string; + }; + }; + icon: { + default: string; + secondary: string; + }; + }; +} + +export const RESOURCE_TYPES = { + DESIGNS: 'design', + FILTERS: 'filter', + VIEWS: 'view' +}; + +export type ContentClassType = { + community: { + icon: React.ComponentType; + color: string; + }; + official: { + icon: React.ComponentType; + color: string; + }; + verified: { + icon: React.ComponentType; + color: string; + }; +}; + +export type UserProfile = { + first_name: string; + last_name: string; + avatar_url: string; +}; diff --git a/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx b/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx new file mode 100644 index 000000000..c3e36e9ce --- /dev/null +++ b/src/custom/CatalogFilterSection/CatalogFilterSidebar.tsx @@ -0,0 +1,162 @@ +import FilterAltIcon from '@mui/icons-material/FilterAlt'; +import { useTheme } from '@mui/material/styles'; +import { useCallback, useState } from 'react'; +import { Box, Drawer, Typography } from '../../base'; +import { CloseIcon } from '../../icons'; +import { darkTeal } from '../../theme'; +import { SLIGHT_BLUE } from '../../theme/colors/colors'; +import { CloseBtn } from '../Modal'; +import CatalogFilterSidebarState from './CatalogFilterSidebarState'; +import { + FilterButton, + FilterDrawerDiv, + FilterText, + FiltersCardDiv, + FiltersDrawerHeader +} from './style'; + +export interface FilterOption { + value: string; + label: string; + totalCount?: number; + description?: string; + Icon?: React.ComponentType<{ + width: string; + height: string; + }>; +} + +export interface FilterList { + filterKey: string; + sectionDisplayName?: string; + defaultOpen?: boolean; + isMultiSelect?: boolean; + options?: FilterOption[]; + customComponent?: React.ComponentType; +} + +type FilterListWithOptions = FilterList & { options: FilterOption[]; customComponent?: never }; + +type FilterListWithCustomComponent = FilterList & { + customComponent: React.ComponentType; + options?: never; +}; + +export type FilterListType = FilterListWithOptions | FilterListWithCustomComponent; + +export interface CatalogFilterSidebarProps { + setData: (callback: (prevFilters: FilterValues) => FilterValues) => void; + lists: FilterListType[]; + value?: FilterValues; + styleProps?: StyleProps; +} + +export type FilterValues = Record; + +export interface StyleProps { + backgroundColor?: string; + sectionTitleBackgroundColor?: string; + fontFamily?: string; +} + +/** + * @function CatalogFilterSidebar + * @description A functional component that renders the filter sidebar. + * @param {Array} value - The data to be filtered. + * @param {Function} setData - A function to set the filtered data. + * @param {Array} lists - An array of filter sections and its options lists. + */ +const CatalogFilterSidebar: React.FC = ({ + lists, + setData, + value = {}, + styleProps +}) => { + const theme = useTheme(); // Get the current theme + const [openDrawer, setOpenDrawer] = useState(false); + + const handleDrawerOpen = useCallback(() => { + setOpenDrawer(true); + }, []); + + const handleDrawerClose = useCallback(() => { + setOpenDrawer(false); + }, []); + + const defaultStyleProps: StyleProps = { + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.background.default + : theme.palette.background.secondary, + sectionTitleBackgroundColor: + theme.palette.mode === 'light' ? theme.palette.background.surfaces : darkTeal.main, + fontFamily: theme.typography.fontFamily + }; + + const appliedStyleProps = { + ...defaultStyleProps, + ...styleProps + }; + + return ( + <> + + + + + + + Filters + + + + + + + Filters + + + + + + + + + + + + + > + ); +}; + +export default CatalogFilterSidebar; diff --git a/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx b/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx new file mode 100644 index 000000000..1d2bb51ba --- /dev/null +++ b/src/custom/CatalogFilterSection/CatalogFilterSidebarState.tsx @@ -0,0 +1,115 @@ +import { useCallback, useState } from 'react'; +import { + CatalogFilterSidebarProps, + FilterListType, + FilterValues, + StyleProps +} from './CatalogFilterSidebar'; +import FilterSection from './FilterSection'; + +/** + * @component CatalogFilterSidebarState + * @description A functional component that manages the filter state. + * @param {Array} lists - An array of filter sections and its options lists. + * @param {Function} onApplyFilters - A function to apply the filters. + * @param {Object} value - The selected filters. + * @param {Object} styleProps - The style properties for the component. + */ +const CatalogFilterSidebarState: React.FC<{ + lists: FilterListType[]; + onApplyFilters: CatalogFilterSidebarProps['setData']; + value: FilterValues; + styleProps: StyleProps; +}> = ({ lists, onApplyFilters, value, styleProps }) => { + // Generate initial state with all sections open by default + const [openSections, setOpenSections] = useState>(() => { + const initialOpenSections: Record = {}; + lists.forEach((list) => { + initialOpenSections[list.filterKey] = !!list.defaultOpen; + }); + return initialOpenSections; + }); + + /** + * @function handleSectionToggle + * @description Handles the section toggle event. + * @param {string} filterKey - The name of the filter section. + */ + const handleSectionToggle = useCallback((filterKey: string) => { + setOpenSections((prevOpenSections) => ({ + ...prevOpenSections, + [filterKey]: !prevOpenSections[filterKey] + })); + }, []); + + /** + * @function handleCheckboxChange + * @description Handles the checkbox change event. + * @param {string} filterKey - The name of the filter section. + * @param {string} value - The value of the checkbox. + * @param {boolean} checked - The checked state of the checkbox. + */ + const handleCheckboxChange = useCallback( + (filterKey: string, value: string, checked: boolean) => { + onApplyFilters((prevFilters) => { + const updatedFilters = { ...prevFilters }; + const filterList = lists.find((list) => list.filterKey === filterKey); + + // default is multi select + if (filterList?.isMultiSelect !== false) { + let currentValues = updatedFilters[filterKey] as string[] | undefined; + + if (!Array.isArray(currentValues)) { + currentValues = currentValues ? [currentValues as string] : []; // convert to array; + } + + updatedFilters[filterKey] = checked + ? [...currentValues, value] + : currentValues.filter((item) => item !== value); + } else { + updatedFilters[filterKey] = checked ? value : ''; + } + + return updatedFilters; + }); + }, + [lists, onApplyFilters] + ); + + return ( + <> + {lists.map((list) => { + if (list.customComponent) { + return ( + + ); + } + + return ( + + ); + })} + > + ); +}; + +export default CatalogFilterSidebarState; diff --git a/src/custom/CatalogFilterSection/FilterSection.tsx b/src/custom/CatalogFilterSection/FilterSection.tsx new file mode 100644 index 000000000..4be5eddbd --- /dev/null +++ b/src/custom/CatalogFilterSection/FilterSection.tsx @@ -0,0 +1,136 @@ +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useCallback, useState } from 'react'; +import { Box, Checkbox, Collapse, List, Stack, Typography } from '../../base'; +import { InfoTooltip } from '../CustomTooltip'; +import { StyledSearchBar } from '../StyledSearchBar'; +import { FilterOption, FilterValues, StyleProps } from './CatalogFilterSidebar'; +import { EndAdornmentText, FilterTitleButton } from './style'; + +interface FilterSectionProps { + filterKey: string; + sectionDisplayName?: string; + options?: FilterOption[]; + filters: FilterValues; + openSections: Record; + onCheckboxChange?: (filterKey: string, value: string, checked: boolean) => void; + onSectionToggle: (filterKey: string) => void; + styleProps: StyleProps; + customComponent?: React.ComponentType; +} + +/** + * @component FilterSection + * @description A functional component that renders a filter section. + * @param {string} filterKey - The key of the filter section. + * @param {string} sectionDisplayName - The title of the filter section. + * @param {Array} options - The available options for the filter section. + * @param {Object} filters - The selected filters. + * @param {Object} openSections - The open/closed state of the filter sections. + * @param {Function} onCheckboxChange - A function to handle checkbox change event. + * @param {Function} onSectionToggle - A function to handle section toggle event. + * @param {Object} styleProps - The style properties for the component. + */ +const FilterSection: React.FC = ({ + filterKey, + sectionDisplayName, + options = [], + filters, + openSections, + onCheckboxChange, + onSectionToggle, + styleProps, + customComponent: CustomComponent +}) => { + const [searchQuery, setSearchQuery] = useState(''); + + const handleTextFieldChange = useCallback((e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }, []); + + const showSearch = options.length > 10; + const searchedOptions = + searchQuery && options.length + ? options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())) + : options; + + return ( + <> + onSectionToggle(filterKey)} + style={{ + backgroundColor: styleProps.sectionTitleBackgroundColor + }} + > + + {(sectionDisplayName || filterKey).toUpperCase()} + + {openSections[filterKey] ? : } + + + {CustomComponent ? ( + + ) : ( + + {showSearch && ( + + Total : {searchedOptions.length ?? 0} + } + /> + + )} + {searchedOptions.map((option, index) => ( + + + + onCheckboxChange && + onCheckboxChange(filterKey, option.value, e.target.checked) + } + value={option.value} + /> + + {option.Icon && } + + {option.label} + + + {option.totalCount !== undefined && `(${option.totalCount || 0})`} + {option.description && ( + + )} + + + ))} + + )} + + > + ); +}; + +export default FilterSection; diff --git a/src/custom/CatalogFilterSection/index.tsx b/src/custom/CatalogFilterSection/index.tsx new file mode 100644 index 000000000..5fb7338c4 --- /dev/null +++ b/src/custom/CatalogFilterSection/index.tsx @@ -0,0 +1,4 @@ +import CatalogFilterSidebar, { FilterListType } from './CatalogFilterSidebar'; + +export { CatalogFilterSidebar }; +export type { FilterListType }; diff --git a/src/custom/CatalogFilterSection/style.tsx b/src/custom/CatalogFilterSection/style.tsx new file mode 100644 index 000000000..32e74de60 --- /dev/null +++ b/src/custom/CatalogFilterSection/style.tsx @@ -0,0 +1,79 @@ +import { styled } from '@mui/material/styles'; +import { Box, Button, ListItemButton } from '../../base'; +import { SLIGHT_BLUE } from '../../theme/colors/colors'; +import { StyleProps } from './CatalogFilterSidebar'; + +export const FiltersCardDiv = styled(Box)<{ styleProps: StyleProps }>(({ styleProps }) => ({ + padding: '1rem', + borderRadius: '1rem', + width: '100%', + gap: '0.5rem', + boxShadow: '0px 2px 10px rgba(0, 0, 0, 0.2)', + display: 'flex', + flexDirection: 'column', + height: 'fit-content', + backgroundColor: styleProps.backgroundColor, + ['@media (max-width:900px)']: { + display: 'none' + }, + fontFamily: styleProps.fontFamily +})); + +export const FilterDrawerDiv = styled('div')(() => ({ + display: 'none', + ['@media (max-width:899px)']: { + display: 'block' + } +})); + +export const LabelDiv = styled('div')(() => ({ + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center' +})); + +export const FilterButton = styled(Button)(({ theme }) => ({ + backgroundColor: theme.palette.primary.brand?.default, + '&:hover': { + backgroundColor: theme.palette.background.default + }, + height: '3.75rem', + ['@media (max-width:450px)']: { + minWidth: '0px' + } +})); + +export const FiltersDrawerHeader = styled(Box)(() => ({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '0.5rem 1rem', + backgroundColor: SLIGHT_BLUE, + height: '10vh' +})); + +export const CheckBoxButton = styled(ListItemButton)(({ theme }) => ({ + padding: '0.25rem 2rem', + borderBottom: '1px solid', + borderBottomColor: theme.palette.text.disabled +})); + +export const FilterTitleButton = styled(ListItemButton)(({ theme }) => ({ + backgroundColor: theme.palette.background.surfaces, + borderRadius: '0.5rem', + marginTop: 2, + display: 'flex', + justifyContent: 'space-between' +})); + +export const EndAdornmentText = styled('p')(({ theme }) => ({ + color: theme.palette.text.tertiary +})); + +export const FilterText = styled('span')(() => ({ + marginLeft: '0.5rem', + display: 'block', + '@media (max-width: 853px)': { + display: 'none' + } +})); diff --git a/src/custom/CustomCatalog/CatalogCardDesignLogo.tsx b/src/custom/CustomCatalog/CatalogCardDesignLogo.tsx new file mode 100644 index 000000000..30e3473f4 --- /dev/null +++ b/src/custom/CustomCatalog/CatalogCardDesignLogo.tsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import { Dialog } from '../../base'; +import { DesignIcon, MesheryFilterIcon } from '../../icons'; + +interface CatalogCardDesignLogoProps { + zoomEffect?: boolean; + imgURL?: string[]; + type: { type: string }; + width: string; + height: string; + style?: React.CSSProperties; +} + +const CatalogCardDesignLogo: React.FC = ({ + zoomEffect = false, + imgURL, + type, + width, + height, + style = {} +}) => { + const [imgError, setImgError] = useState(false); + const [isZoomed, setIsZoomed] = useState(false); + + const handleZoomClick = () => { + if (zoomEffect) { + setIsZoomed(true); + } + }; + + const handleZoomClose = () => { + setIsZoomed(false); + }; + + const SvgComponent: React.FC<{ type: { type: string } }> = ({ type }) => { + return type.type === 'filter' ? ( + + ) : ( + + ); + }; + + return ( + <> + {imgURL && imgURL.length > 0 ? ( + + {!imgError ? ( + <> + setImgError(true)} + style={{ + cursor: 'pointer', + width: '100%', + height: '100%', + objectFit: 'cover' + }} + /> + + + + > + ) : ( + + )} + + ) : ( + + )} + > + ); +}; + +export default CatalogCardDesignLogo; diff --git a/src/custom/CustomCatalog/CustomCard.tsx b/src/custom/CustomCatalog/CustomCard.tsx new file mode 100644 index 000000000..13233bb6c --- /dev/null +++ b/src/custom/CustomCatalog/CustomCard.tsx @@ -0,0 +1,332 @@ +import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; +import React, { useEffect, useState } from 'react'; +import { Avatar, Grid } from '../../base'; +import { CloneIcon, CommunityClassIcon, OfficialClassIcon, OpenIcon, ShareIcon } from '../../icons'; +import VerificationClassIcon from '../../icons/ContentClassIcons/VerificationClassIcon'; +import DeploymentsIcon from '../../icons/Deployments/DeploymentsIcon'; +import { DownloadIcon } from '../../icons/Download'; +import { DARK_TEAL, styled, useTheme } from '../../theme'; +import { SNOW_WHITE } from '../../theme/colors/colors'; +import { CustomTooltip } from '../CustomTooltip'; +import { getVersion, handleImage } from './Helper'; +import { + CardBack, + CardFront, + DateText, + DateType, + DesignAuthorName, + DesignCard, + DesignDetailsDiv, + DesignInnerCard, + DesignName, + DesignType, + MetricsContainerFront, + MetricsCount, + MetricsDiv, + NoTechnologyText, + ProfileSection, + StyledClassWrapper, + StyledInnerClassWrapper, + TechnologiesSection, + TechnologyText, + VersionDiv, + VersionText +} from './style'; + +export const DesignCardUrl = styled('a')(() => ({ + textDecoration: 'none' +})); + +export interface Pattern { + id: string; + user_id: string; + pattern_file: string; + user: { + first_name: string; + last_name: string; + }; + first_name?: string; + last_name?: string; + avatar_url: string; + name: string; + download_count: number; + clone_count: number; + view_count: number; + deployment_count: number; + share_count: number; + userData?: { + version?: string; + avatarUrl?: string; + userName?: string; + technologies?: string[]; + updatedAt?: string; + }; + catalog_data?: { + content_class?: string; + imageURL?: string[]; + compatibility?: string[]; + published_version?: string; + type?: string; + pattern_info?: string; + pattern_caveats?: string; + }; + visibility: string; + updated_at: Date; + created_at: Date; +} + +type CatalogCardProps = { + pattern: Pattern; + patternType: string; + cardHeight?: string; + cardWidth?: string; + cardStyles?: React.CSSProperties; + avatarUrl?: string; + shouldFlip?: boolean; + cardTechnologies?: boolean; + isDetailed?: boolean; + UserName?: string; + children?: React.ReactNode; // catalogImage + basePath?: string; // path of meshmodel img stored + subBasePath?: string; // path of meshmodel img stored + getHostUrl?: () => string; + onCardClick?: () => void; +}; + +export const ClassToIconMap = { + community: , + official: , + verified: +}; + +const ClassWrap = ({ catalogClassName }: { catalogClassName: string }) => { + if (!catalogClassName) return <>>; + + return ( + + + {catalogClassName} + + + ); +}; + +const CustomCatalogCard: React.FC = ({ + pattern, + patternType, + cardHeight = '18rem', + cardWidth = '15rem', + cardStyles, + shouldFlip = true, + isDetailed = true, + cardTechnologies = true, + avatarUrl, + UserName, + children, + basePath, + subBasePath, + getHostUrl, + onCardClick +}) => { + const outerStyles = { + height: cardHeight, + width: cardWidth, + ...cardStyles + }; + const theme = useTheme(); + + const technologies = pattern.catalog_data?.compatibility || []; + const techlimit = 5; + const [availableTechnologies, setAvailableTechnologies] = useState([]); + const version = getVersion(pattern); + + useEffect(() => { + handleImage({ + technologies, + basePath, + subBasePath, + setAvailableTechnologies + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!shouldFlip) { + return ( + + + + {children} + + + + ); + } + + return ( + + + + {isDetailed && ( + <> + + {patternType} + {pattern.name} + > + )} + + + {children} + + + {isDetailed && ( + + {[ + { Icon: DownloadIcon, count: pattern.download_count }, + { Icon: CloneIcon, count: pattern.clone_count }, + { Icon: OpenIcon, count: pattern.view_count }, + { Icon: DeploymentsIcon, count: pattern.deployment_count }, + { Icon: ShareIcon, count: pattern.share_count } + ].map(({ Icon, count }, index) => ( + + + {count} + + ))} + + )} + + {shouldFlip && ( + + + + {UserName} + + + + {cardTechnologies && ( + + Technologies + + {technologies.length < 1 || availableTechnologies.length < 1 ? ( + No technologies + ) : ( + <> + {availableTechnologies.slice(0, techlimit).map((technology, index) => { + const svgPath = + (getHostUrl ? getHostUrl() : '') + + `${basePath}/${technology.toLowerCase()}/${subBasePath}/${technology.toLowerCase()}-color.svg`; + return ( + + + + + + ); + })} + {availableTechnologies.length > techlimit && ( + + +{availableTechnologies.length - techlimit} + + )} + > + )} + + + )} + + + {isDetailed && ( + + + + + + Updated At + + + {' '} + {new Date(pattern.updated_at.toString().slice(0, 10)).toLocaleDateString( + 'en-US', + { + day: 'numeric', + month: 'long', + year: 'numeric' + } + )} + + + + + )} + {version && ( + + v{version} + + )} + + )} + + + ); +}; + +export default CustomCatalogCard; diff --git a/src/custom/CustomCatalog/EmptyStateCard.tsx b/src/custom/CustomCatalog/EmptyStateCard.tsx new file mode 100644 index 000000000..256b193d3 --- /dev/null +++ b/src/custom/CustomCatalog/EmptyStateCard.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react'; +import { EmptyStyleIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { CatalogEmptyStateDiv } from './style'; + +const EmptyStateCard: FC = () => { + const theme = useTheme(); + return ( + + + No match found + + ); +}; + +export default EmptyStateCard; diff --git a/src/custom/CustomCatalog/Helper.ts b/src/custom/CustomCatalog/Helper.ts new file mode 100644 index 000000000..02c5d0c54 --- /dev/null +++ b/src/custom/CustomCatalog/Helper.ts @@ -0,0 +1,74 @@ +import jsyaml from 'js-yaml'; +import { Pattern } from './CustomCard'; + +const checkImageUrlValidity = async ( + url: string, + appendHostUrl = true, + getHostUrl?: () => string +): Promise => { + return new Promise((resolve) => { + const img = new Image(); + // Only append host if the URL does not start with "http" or "https" + if (appendHostUrl && !url.startsWith('http')) { + img.src = (getHostUrl ? getHostUrl() : '') + url; + } else { + img.src = url; + } + img.onload = () => { + // Check if the image loaded successfully + resolve(true); + }; + + img.onerror = () => { + // Handle the case where the image could not be loaded + resolve(false); + }; + }); +}; + +const getValidSvgPaths = async ( + technologies: string[], + basePath: string, + subBasePath: string +): Promise => { + const validSvgPaths: string[] = []; + for (const technology of technologies) { + const svgIconPath = `${basePath}/${technology.toLowerCase()}/${subBasePath}/${technology.toLowerCase()}-color.svg`; + const isSvgPathValid = await checkImageUrlValidity(svgIconPath as string); + if (isSvgPathValid) { + validSvgPaths.push(technology); + } + } + return validSvgPaths; +}; + +interface HandleImageProps { + technologies: string[]; + basePath?: string; + subBasePath?: string; + setAvailableTechnologies: (technologies: string[]) => void; +} + +export const handleImage = async ({ + technologies, + basePath = '', + subBasePath = '', + setAvailableTechnologies +}: HandleImageProps) => { + const validSvgPaths = await getValidSvgPaths(technologies, basePath, subBasePath); + setAvailableTechnologies(validSvgPaths); +}; +export const DEFAULT_DESIGN_VERSION = '0.0.0'; + +export const getVersion = (design: Pattern) => { + if (design.visibility === 'published') { + return design?.catalog_data?.published_version || DEFAULT_DESIGN_VERSION; + } + try { + const patternFile = jsyaml.load(design.pattern_file); + return patternFile?.version || DEFAULT_DESIGN_VERSION; + } catch (e) { + console.error(e); + return DEFAULT_DESIGN_VERSION; + } +}; diff --git a/src/custom/CustomCatalog/index.tsx b/src/custom/CustomCatalog/index.tsx new file mode 100644 index 000000000..be7c47128 --- /dev/null +++ b/src/custom/CustomCatalog/index.tsx @@ -0,0 +1,5 @@ +import CatalogCardDesignLogo from './CatalogCardDesignLogo'; +import CustomCatalogCard from './CustomCard'; +import EmptyStateCard from './EmptyStateCard'; + +export { CatalogCardDesignLogo, CustomCatalogCard, EmptyStateCard }; diff --git a/src/custom/CustomCatalog/style.tsx b/src/custom/CustomCatalog/style.tsx new file mode 100644 index 000000000..38fb80054 --- /dev/null +++ b/src/custom/CustomCatalog/style.tsx @@ -0,0 +1,435 @@ +import { styled, Typography } from '@mui/material'; +import { accentGrey, DARK_PRIMARY_COLOR, GRAY97, slateGray, WHITESMOKE } from '../../theme'; +import { charcoal, DARK_TEAL, SNOW_WHITE } from '../../theme/colors/colors'; + +type DesignCardProps = { + outerStyles: React.CSSProperties; + shouldFlip?: boolean; + isDetailed?: boolean; +}; +type DesignCardDivProps = { + shouldFlip?: boolean; + isDetailed?: boolean; +}; +type MetricsProps = { + isDetailed?: boolean; +}; +type CatalogProps = { + isCatalog?: boolean; +}; +type StyledInnerClassWrapperProps = { + catalogClassName: string; +}; +export const StyledClassWrapper = styled('div')(() => ({ + width: '85px', + height: '88px', + overflow: 'hidden', + position: 'absolute', + top: '-3px', + left: '-3px' +})); + +export const TechnologyText = styled('div')(() => ({ + color: '#eee', + fontSize: '0.875rem', + lineHeight: '1.5', + fontWeight: '600', + borderBottom: '1px solid rgba(231, 239, 243, 0.40)' +})); + +export const NoTechnologyText = styled('div')(() => ({ + color: '#eee', + overflow: 'hidden', + fontSize: '14px', + lineHeight: '24px', + fontWeight: '400', + marginTop: '.8rem' +})); + +export const StyledInnerClassWrapper = styled('div')(({ + catalogClassName, + theme +}) => { + const mapToColor: Record = { + community: slateGray.main, + official: theme.palette.background.cta?.default || '#EBC017', + verified: theme.palette.background.brand?.default || '#00B39F' + }; + return { + font: 'bold 10px sans-serif', + WebkitTransform: 'rotate(-45deg)', + textAlign: 'center', + transform: 'rotate(-45deg)', + position: 'relative', + padding: '4px 0', + top: '15px', + left: '-30px', + width: '120px', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: mapToColor[catalogClassName], + color: catalogClassName === 'official' ? theme.palette.common.black : theme.palette.common.white + }; +}); + +export const DesignCard = styled('div')( + ({ shouldFlip, isDetailed, outerStyles, theme }) => ({ + position: 'relative', + borderRadius: '1rem', + textAlign: 'center', + transformStyle: 'preserve-3d', + display: 'block', + perspective: '1000px', + transition: 'all .9s ease-out', + ...(shouldFlip && { + '&:hover': { + cursor: 'pointer', + '& .innerCard': { + transform: 'rotateY(180deg)' + } + } + }), + ...(isDetailed && { + [theme.breakpoints.down('lg')]: { + height: '18.75rem' + } + }), + ...outerStyles + }) +); + +export const DesignInnerCard = styled('div')(({ shouldFlip, isDetailed }) => ({ + position: 'relative', + width: '100%', + height: '100%', + textAlign: 'center', + transition: 'transform 0.6s', + ...(shouldFlip && { + transformOrigin: '50% 50%', + transformStyle: 'preserve-3d' + }), + ...(isDetailed && { + boxShadow: '0 4px 8px 0 rgba(0,0,0,0.2)', + borderRadius: '0.9375rem' + }) +})); + +export const DesignType = styled('span')(({ theme }) => ({ + position: 'absolute', + top: '0', + right: '0', + minWidth: '3rem', + padding: '0 0.75rem', + fontSize: '0.875rem', + textTransform: 'capitalize', + background: theme.palette.background.brand?.default, + color: theme.palette.background.constant?.white, + borderRadius: '0 1rem 0 2rem' +})); +export const MetricsCount = styled('p')(({ theme }) => ({ + fontSize: '1rem', + textTransform: 'capitalize', + margin: '0rem', + lineHeight: '1.5', + textAlign: 'center', + color: theme.palette.mode === 'light' ? DARK_TEAL : SNOW_WHITE, + fontWeight: '600' +})); +export const DesignName = styled(Typography)(({ theme }) => ({ + fontWeight: 'bold', + textTransform: 'capitalize', + color: theme.palette.text.default, + fontSize: '1.125rem', + marginTop: '2rem', + padding: '0rem 1rem', + position: 'relative', + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + textAlign: 'center', + width: '100%', + margin: '3rem 0 1.59rem 0', + fontFamily: 'inherit' +})); + +export const MetricsContainerFront = styled('div')(({ isDetailed, theme }) => ({ + display: 'flex', + justifyContent: 'space-around', + fontSize: '0.2rem', + color: theme.palette.mode === 'light' ? 'rgba(26, 26, 26, .8)' : theme.palette.text.default, + padding: '0.9rem 0.1rem', + background: theme.palette.mode === 'light' ? '#E7EFF3' : DARK_TEAL, + ...(isDetailed && { + position: 'absolute', + bottom: '0px' + }), + ...(!isDetailed && { + marginTop: '1.2rem' + }), + borderRadius: '0 0 0.9375rem 0.9375rem', + width: '100%' +})); + +export const MetricsDiv = styled('div')(() => ({ + display: 'flex', + alignItems: 'center', + gap: '4px', + fontSize: '0.2rem', + color: 'rgba(26, 26, 26, .8)', + margin: '0rem', + padding: '0.1rem' +})); +export const DesignDetailsDiv = styled('div')(() => ({ + height: 'max-content', + display: 'flex', + marginTop: '-1rem', + flexDirection: 'column', + padding: '0rem 1rem', + justifyContent: 'start', + alignItems: 'start', + ['@media (max-width:1200px)']: { + height: 'max-content' + } +})); + +export const ImageWrapper = styled('div')(({ theme }) => ({ + background: theme.palette.background.surfaces, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + padding: '0.5rem', + width: '100%', + borderRadius: '0.5rem' +})); + +export const VersionTag = styled('div')(({ theme }) => ({ + display: 'inline-block', + backgroundColor: theme.palette.background.supplementary, + color: theme.palette.text.constant?.white, + borderRadius: '4px', + fontSize: '0.75rem', + fontWeight: 'bold', + margin: '5px 0', + padding: '2px 5px', + maxWidth: 'fit-content' +})); + +export const VersionDiv = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: '4px', + fontSize: '0.75', + color: theme.palette.text.constant?.white, + position: 'absolute', + bottom: '16px', + left: '16px', + borderRadius: '4px', + background: theme.palette.background.supplementary, + justifyContent: 'center' +})); + +export const VersionText = styled('p')(({ theme }) => ({ + fontSize: '0.75rem', + margin: '0', + padding: '0.25rem .5rem', + lineHeight: '1.5', + textTransform: 'lowercase', + fontWeight: '600', + borderRadius: '4.05px', + color: theme.palette.text.constant?.white +})); + +export const FlipCard = styled('div')(() => ({ + perspective: '1000px', + '&:hover .flipper': { + transform: 'rotateY(-180deg)' + } +})); + +export const Flipper = styled('div')(() => ({ + transition: '0.6s', + transformStyle: 'preserve-3d', + position: 'relative' +})); + +export const Face = styled('div')(() => ({ + backfaceVisibility: 'hidden', + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%' +})); + +export const FrontFace = styled(Face)(() => ({ + zIndex: 2, + transform: 'rotateY(0deg)' +})); + +export const BackFace = styled('div')(() => ({ + transform: 'rotateY(-180deg)', + color: '#fff', + display: 'inline-flex', + flexDirection: 'column', + padding: '16px', + height: '100%', + width: '100%', + position: 'relative', + bottom: 0, + left: 0, + backfaceVisibility: 'hidden' +})); + +export const BackFaceContent = styled('div')(({ theme }) => ({ + position: 'absolute', + background: `linear-gradient(to bottom right, black 40%, ${theme.palette.background.brand?.default})`, + width: '100%', + top: 0, + left: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'left', + padding: '16px', + boxShadow: `2px 2px 3px 0px black`, + borderRadius: '1rem' +})); + +export const ProfileSection = styled('div')({ + height: 'max-content', + display: 'flex', + marginTop: '1.2rem', + flexDirection: 'row', + padding: '0rem 1rem', + justifyContent: 'flex-start', + alignItems: 'center', + ['@media (max-width:1200px)']: { + height: 'max-content' + } +}); + +export const TechnologiesSection = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + width: '100%', + gap: '1rem', + alignItems: 'flex-start', + background: 'rgba(231, 239, 243, 0.40)', + borderRadius: '0.25rem', + padding: '0.5rem 1rem', + alignSelf: 'stretch' +})); + +export const UpdatedSection = styled('div')({ + display: 'flex', + alignItems: 'center', + color: '#fff', + margin: '20px 0' +}); + +export const CardBack = styled('div')(({ isCatalog }) => ({ + boxShadow: `2px 2px 3px 0px rgba(26, 26, 26, 1)`, + position: 'absolute', + width: '100%', + height: '100%', + WebkitBackfaceVisibility: 'hidden', + borderRadius: '0.9375rem', + backfaceVisibility: 'hidden', + color: 'white', + transform: 'rotateY(180deg)', + ...(isCatalog && { + background: + 'linear-gradient(335deg, rgba(0, 179, 159, 0.80) -13.6%, rgba(0, 0, 0, 0.68) 66.8%), radial-gradient(3970.04% 147.22% at 47.5% 100%, #000 0%, #395357 100%)' + }), + ...(!isCatalog && { + background: 'linear-gradient(250deg, #477e96 0%, #00b39f 35%, rgb(60, 73, 79) 100%)' + }) +})); + +const getBackground = (isLightMode: boolean) => { + const lightGradient = `linear-gradient(to left bottom, ${WHITESMOKE}, ${GRAY97},white, white, white, white, white, white, white, white, ${WHITESMOKE}, ${GRAY97})`; + const darkGradient = `linear-gradient(to right top, ${DARK_PRIMARY_COLOR}, ${accentGrey[30]}, ${accentGrey[20]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${accentGrey[10]}, ${charcoal[20]}, ${charcoal[10]}, black)`; + + return isLightMode ? lightGradient : darkGradient; +}; +export const CardFront = styled('div')(({ shouldFlip, isDetailed, theme }) => { + const isLightMode = theme.palette.mode === 'light'; + const background = getBackground(isLightMode); + const boxShadow = `2px 2px 3px 0px ${theme.palette.background.brand?.default}`; + + return { + ...(shouldFlip && { + position: 'absolute', + boxShadow, + background + }), + ...(isDetailed && { + boxShadow, + background + }), + width: '100%', + height: '100%', + WebkitBackfaceVisibility: 'hidden', + borderRadius: '0.9375rem', + backfaceVisibility: 'hidden' + }; +}); + +export const DateText = styled('div')(() => ({ + fontSize: '0.875rem', + textTransform: 'capitalize', + color: '#eee', + margin: '0rem', + padding: '0.1rem', + fontWeight: '400', + lineHeight: '1.5' +})); + +export const DateType = styled('p')(() => ({ + fontSize: '0.876rem', + margin: '0rem', + lineHeight: '1.5', + fontWeight: '400', + color: '#eee' +})); + +export const DesignAuthorName = styled('div')(() => ({ + height: 'max-content', + display: 'flex', + margin: '0', + flexDirection: 'column', + padding: '0rem 1rem', + justifyContent: 'start', + alignItems: 'start', + fontWeight: '400', + textAlign: 'right', + color: '#E7EFF3', + textTransform: 'capitalize', + ['@media (max-width:1200px)']: { + height: 'max-content' + } +})); + +export const CatalogEmptyStateDiv = styled('div')(({ theme }) => { + const isLightMode = theme.palette.mode === 'light'; + const background = getBackground(isLightMode); + const boxShadow = `2px 2px 3px 0px ${theme.palette.background.brand?.default}`; + + return { + background: background, + boxShadow: boxShadow, + textAlign: 'center', + borderRadius: '1rem', + width: '15rem', + height: '18rem', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + [theme.breakpoints.down('lg')]: { + height: '18.75rem' + } + }; +}); diff --git a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx index faf616459..7541891d1 100644 --- a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx +++ b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx @@ -1,4 +1,3 @@ -import { useTheme } from '@mui/material'; import React from 'react'; import { Box } from '../../base/Box'; import { Card } from '../../base/Card'; @@ -6,6 +5,7 @@ import { Checkbox } from '../../base/Checkbox'; import { ClickAwayListener } from '../../base/ClickAwayListener'; import { FormControlLabel } from '../../base/FormControlLabel'; import { ColumnIcon } from '../../icons'; +import { useTheme } from '../../theme'; import PopperListener from '../PopperListener'; import TooltipIcon from '../TooltipIcon'; @@ -27,7 +27,8 @@ export interface CustomColumn { export function CustomColumnVisibilityControl({ columns, id, - customToolsProps + customToolsProps, + style }: CustomColumnVisibilityControlProps): JSX.Element { const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -60,6 +61,7 @@ export function CustomColumnVisibilityControl({ arrow /> - {children} + {children} ); } diff --git a/src/custom/ErrorBoundary/ErrorBoundary.tsx b/src/custom/ErrorBoundary/ErrorBoundary.tsx index 513453070..a47c3e8cb 100644 --- a/src/custom/ErrorBoundary/ErrorBoundary.tsx +++ b/src/custom/ErrorBoundary/ErrorBoundary.tsx @@ -20,6 +20,8 @@ const StyledLink = styled(Link)(({ theme }) => ({ })); const CodeMessage = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', backgroundColor: theme.palette.background.code, color: theme.palette.text.tertiary, padding: '.85rem', @@ -30,14 +32,32 @@ const CodeMessage = styled('div')(({ theme }) => ({ interface FallbackComponentProps extends FallbackProps { resetErrorBoundary: () => void; children?: React.ReactNode; + pageUrl?: string; + timestamp?: string; + showPackageInfo?: boolean; + version?: string; } -export function Fallback({ error, children }: FallbackComponentProps): JSX.Element { +export function Fallback({ + error, + children, + showPackageInfo, + version +}: FallbackComponentProps): JSX.Element { return ( Uh-oh!😔 Please pardon the mesh. - {(error as Error).message} + + Error: + {(error as Error).message} + + + {showPackageInfo && ( + <> + Version: {version} + > + )} We apologize for the inconvenience. The issue may be on our end. If troubleshooting doesn't @@ -56,18 +76,50 @@ export function Fallback({ error, children }: FallbackComponentProps): JSX.Eleme } const reportError = (error: Error, info: React.ErrorInfo): void => { + const pageUrl = window.location.href; + const timestamp = new Date().toLocaleString(); // This is where you'd send the error to Sentry, etc - console.log('Error Caught Inside Boundary --reportError', error, 'Info', info); + console.log( + 'Error Caught Inside Boundary --reportError', + error, + 'Info', + info, + 'Page URL:', + pageUrl, + 'Timestamp:', + timestamp + ); }; interface ErrorBoundaryProps { customFallback?: React.ComponentType; children: React.ReactNode; + onErrorCaught?: (error: string) => void; } -export const ErrorBoundary: React.FC = ({ customFallback, children }) => { +export const ErrorBoundary: React.FC = ({ + customFallback, + children, + onErrorCaught +}) => { + const pageUrl = window.location.href; + const timestamp = new Date().toLocaleString(); + + const handleError = (error: Error, info: React.ErrorInfo) => { + // Pass error message to onErrorCaught + onErrorCaught?.(error.message); + reportError(error, info); + }; + return ( - + + } + onError={handleError} + > {children} ); diff --git a/src/custom/InputSearchField/InputSearchField.tsx b/src/custom/InputSearchField/InputSearchField.tsx new file mode 100644 index 000000000..c6f9c6472 --- /dev/null +++ b/src/custom/InputSearchField/InputSearchField.tsx @@ -0,0 +1,208 @@ +import { Autocomplete } from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Box, Chip, CircularProgress, Grid, TextField, Tooltip, Typography } from '../../base'; +import { iconLarge, iconSmall } from '../../constants/iconsSizes'; +import { CloseIcon, OrgIcon } from '../../icons'; + +interface Option { + id: string; + name: string; +} + +interface InputSearchFieldProps { + data: Option[]; + setFilterData: (data: Option[]) => void; + label?: string; + fetchSuggestions: (value: string) => void; + isLoading: boolean; + type: string; + disabled?: boolean; + selectedData: Option[]; + searchValue: string; + setSearchValue: (value: string) => void; + iconComponent?: React.ReactElement; +} + +const InputSearchField: React.FC = ({ + data, + label, + fetchSuggestions, + setFilterData, + isLoading, + type, + disabled, + selectedData, + searchValue, + setSearchValue, + iconComponent = () as React.ReactElement +}) => { + const [error, setError] = useState(''); + const [open, setOpen] = useState(false); + const [showAllItems, setShowAllItems] = useState(false); + const [localSelectedData, setLocalSelectedData] = useState(selectedData); + + // Sync local state with prop changes + useEffect(() => { + setLocalSelectedData(selectedData); + }, [selectedData]); + + const handleDelete = useCallback( + (id: string) => { + const newData = localSelectedData.filter((item) => item.id !== id); + setLocalSelectedData(newData); + setFilterData(newData); + }, + [localSelectedData, setFilterData] + ); + + const handleAdd = useCallback( + (_event: React.SyntheticEvent, value: Option | null) => { + if (!value) return; + + // Check for duplicates + const isDuplicate = localSelectedData.some((item) => item.id === value.id); + if (isDuplicate) { + setError(`${type} already selected`); + return; + } + + // Update both local and parent state + const newData = [...localSelectedData, value]; + setLocalSelectedData(newData); + setFilterData(newData); + setError(''); + setSearchValue(''); + setOpen(false); + }, + [localSelectedData, setFilterData, type, setSearchValue] + ); + + const handleInputChange = useCallback( + (_event: React.SyntheticEvent, value: string) => { + setSearchValue(value); + if (value === '') { + setOpen(false); + } else { + const encodedValue = encodeURIComponent(value); + fetchSuggestions(encodedValue); + setError(''); + setOpen(true); + } + }, + [fetchSuggestions, setSearchValue] + ); + + return ( + + searchValue} + isOptionEqualToValue={(option: Option, value: Option) => option.id === value.id} + noOptionsText={isLoading ? 'Loading...' : `No ${type} found`} + loading={isLoading} + open={open} + onClose={() => setOpen(false)} + disabled={disabled} + value={undefined} + inputValue={searchValue} + onChange={handleAdd} + onInputChange={handleInputChange} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + filterOptions={(x) => x} + disableClearable + includeInputInList + filterSelectedOptions + disableListWrap + clearOnBlur + popupIcon={null} + blurOnSelect + forcePopupIcon={false} + renderInput={(params) => ( + + {isLoading ? : null} + + ) + }} + /> + )} + renderOption={(props, option: Option) => ( + + img': { mr: 2, flexShrink: 0 } }}> + + + {iconComponent} + + + {option.name} + + + + + )} + /> + + 0 ? '0.5rem' : '' + }} + > + {!showAllItems && localSelectedData?.length > 0 && ( + handleDelete(localSelectedData[localSelectedData.length - 1]?.id)} + deleteIcon={ + + + + } + /> + )} + {showAllItems && + localSelectedData?.map((obj) => ( + } + label={obj.name} + size="small" + onDelete={() => handleDelete(obj.id)} + deleteIcon={ + + + + } + /> + ))} + {localSelectedData?.length > 1 && ( + setShowAllItems(!showAllItems)} + sx={{ + cursor: 'pointer' + }} + > + {showAllItems ? '(hide)' : `(+${localSelectedData?.length - 1})`} + + )} + + + ); +}; + +export default InputSearchField; diff --git a/src/custom/InputSearchField/index.ts b/src/custom/InputSearchField/index.ts new file mode 100644 index 000000000..6ad430f53 --- /dev/null +++ b/src/custom/InputSearchField/index.ts @@ -0,0 +1,3 @@ +import InputSearchField from './InputSearchField'; + +export { InputSearchField }; diff --git a/src/custom/LearningCard/LearningCard.tsx b/src/custom/LearningCard/LearningCard.tsx index 9ef01a1ce..910e6ea19 100644 --- a/src/custom/LearningCard/LearningCard.tsx +++ b/src/custom/LearningCard/LearningCard.tsx @@ -25,11 +25,19 @@ interface Tutorial { interface Props { tutorial: Tutorial; - path: string; + path?: string; courseCount: number; courseType: string; } +const OptionalLink: React.FC> = ({ path, children }) => { + if (!path) { + return <>{children}>; + } + + return {children}; +}; + const LearningCard: React.FC = ({ tutorial, path, courseCount, courseType }) => { return ( @@ -57,7 +65,7 @@ const LearningCard: React.FC = ({ tutorial, path, courseCount, courseType ) : ( - + @@ -88,7 +96,7 @@ const LearningCard: React.FC = ({ tutorial, path, courseCount, courseType - + )} ); diff --git a/src/custom/Markdown/index.tsx b/src/custom/Markdown/index.tsx index 92cbb5fd2..cc0c8ee9b 100644 --- a/src/custom/Markdown/index.tsx +++ b/src/custom/Markdown/index.tsx @@ -2,6 +2,7 @@ import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; import { + BasicAnchorMarkdown, StyledMarkdown, StyledMarkdownBlockquote, StyledMarkdownH1, @@ -97,3 +98,27 @@ export const RenderMarkdownTooltip: React.FC = ({ content } ); }; + +// Markdown support for notifications markdown content +export const BasicMarkdown: React.FC = ({ content }) => { + return ( + ( + { + window.open(props.href, '_blank'); + e.stopPropagation(); + }} + as="a" + > + {props.children} + + ) + }} + > + {content} + + ); +}; diff --git a/src/custom/Markdown/style.tsx b/src/custom/Markdown/style.tsx index aab1940ce..2043d2832 100644 --- a/src/custom/Markdown/style.tsx +++ b/src/custom/Markdown/style.tsx @@ -10,10 +10,20 @@ export const StyledMarkdown = styled('a')(({ theme }) => ({ cursor: 'pointer' })); +// anchor style for notifications markdown content +export const BasicAnchorMarkdown = styled('a')(() => ({ + textDecoration: 'none', + '&:hover': { + textDecoration: 'underline' + }, + cursor: 'pointer' +})); + export const StyledMarkdownP = styled('p')(({ theme }) => ({ color: theme.palette.text.default, marginBlock: '0px', - ...theme.typography.textB1Regular + ...theme.typography.textB1Regular, + fontFamily: 'inherit' })); export const StyledMarkdownTooltipP = styled('p')(({ theme }) => ({ diff --git a/src/custom/Modal/index.tsx b/src/custom/Modal/index.tsx index 5bcfc3276..f551c8d5d 100644 --- a/src/custom/Modal/index.tsx +++ b/src/custom/Modal/index.tsx @@ -4,6 +4,7 @@ import { Box, Dialog, IconButton, Paper, Typography } from '../../base'; import { ContainedButton, OutlinedButton, TextButton } from '../../base/Button/Button'; import { iconLarge, iconMedium } from '../../constants/iconsSizes'; import { CloseIcon, InfoCircleIcon } from '../../icons'; +import { darkModalGradient, lightModalGradient } from '../../theme/colors/colors'; import { CustomTooltip } from '../CustomTooltip'; interface ModalProps extends DialogProps { @@ -54,8 +55,8 @@ const StyledDialog = styled(Dialog)` } `; -export const ModalStyledHeader = styled('div')(() => ({ - background: 'linear-gradient(90deg, #3B687B 0%, #507D90 100%)', +export const ModalStyledHeader = styled('div')(({ theme }) => ({ + background: theme.palette.mode === 'light' ? lightModalGradient.header : darkModalGradient.header, color: '#eee', display: 'flex', justifyContent: 'space-between', @@ -108,7 +109,11 @@ const StyledFooter = styled('div', { shouldForwardProp: (prop) => prop !== 'variant' })(({ theme, variant, hasHelpText }) => ({ background: - variant == 'filled' ? 'linear-gradient(90deg, #3B687B 0%, #507D90 100%)' : 'transparent', + variant === 'filled' + ? theme.palette.mode === 'light' + ? lightModalGradient.fotter + : darkModalGradient.fotter + : 'transparent', display: 'flex', alignItems: 'center', justifyContent: hasHelpText ? 'space-between' : 'end', @@ -116,7 +121,8 @@ const StyledFooter = styled('div', { gap: '1rem', '&& .InfoCircleIcon': { - color: variant == 'filled' ? theme.palette.common.white : theme.palette.background.info?.default + color: + variant === 'filled' ? theme.palette.common.white : theme.palette.background.info?.default } })); diff --git a/src/custom/NavigationNavbar/navigationNavbar.tsx b/src/custom/NavigationNavbar/navigationNavbar.tsx index 6d5f83df5..2ae99e7f1 100644 --- a/src/custom/NavigationNavbar/navigationNavbar.tsx +++ b/src/custom/NavigationNavbar/navigationNavbar.tsx @@ -1,5 +1,6 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { ListItemTextProps, MenuListProps } from '@mui/material'; import React, { MouseEvent, useState } from 'react'; import { Collapse, Divider, ListItemText, MenuItem } from '../../base'; import { IconWrapper, MenuItemList, MenuItemSubList, MenuListStyle, SubIconWrapper } from './style'; @@ -14,13 +15,18 @@ type NavigationItem = { addDivider?: boolean; }; -type NavigationNavbarProps = { +interface NavigationNavbarProps { navigationItems: NavigationItem[]; -}; + MenuListProps?: Omit; + ListItemTextProps?: Omit; +} -const NavigationNavbar: React.FC = ({ navigationItems }) => { +const NavigationNavbar: React.FC = ({ + navigationItems, + MenuListProps = {}, + ListItemTextProps = {} +}) => { const [openSectionId, setOpenSectionId] = useState(null); - const toggleSectionOpen = (sectionId: string, event: MouseEvent) => { event.stopPropagation(); setOpenSectionId((currentOpenSectionId) => @@ -28,50 +34,52 @@ const NavigationNavbar: React.FC = ({ navigationItems }) ); }; - const NavigationNavbarItems = () => { - return navigationItems.map((item) => { - const isOpen = openSectionId === item.id; - const permission = item.permission ?? true; - const addDivider = item.addDivider ?? false; + return ( + + {navigationItems.map((item) => { + const isOpen = openSectionId === item.id; + const permission = item.permission ?? true; + const addDivider = item.addDivider ?? false; - return ( - - - - {item.icon} - - + return ( + + + + {item.icon && {item.icon}} + + + {item.subItems && ( + + {isOpen ? ( + toggleSectionOpen(item.id, e)} /> + ) : ( + toggleSectionOpen(item.id, e)} /> + )} + + )} + {item.subItems && ( - - {isOpen ? ( - toggleSectionOpen(item.id, e)} /> - ) : ( - toggleSectionOpen(item.id, e)} /> - )} - + + {item.subItems.map((subItem) => ( + + + {subItem.icon && {subItem.icon}} + + + + ))} + )} - - - {item.subItems && ( - - {item.subItems.map((subItem) => ( - - - {subItem.icon} - - - - ))} - - )} - - {addDivider && } - - ); - }); - }; - - return {NavigationNavbarItems()}; + {addDivider && } + + ); + })} + + ); }; export default NavigationNavbar; diff --git a/src/custom/NavigationNavbar/style.tsx b/src/custom/NavigationNavbar/style.tsx index d6cd54f1e..c7f0f6998 100644 --- a/src/custom/NavigationNavbar/style.tsx +++ b/src/custom/NavigationNavbar/style.tsx @@ -7,8 +7,6 @@ export const ListItemStyle = styled('div')(({ theme }) => ({ })); export const MenuListStyle = styled(MenuList)({ - minHeight: '31rem', - width: '13rem', overflowY: 'auto', scrollbarWidth: 'none', '&::-webkit-scrollbar': { @@ -18,7 +16,7 @@ export const MenuListStyle = styled(MenuList)({ export const MenuItemList = styled(ListItem)(() => ({ pointerEvents: 'auto', - margin: '0.5rem 0rem 0.5rem 0.5rem', + margin: '0.5rem 0rem 0.5rem 0rem', fontSize: '0.1rem', padding: '0' })); @@ -30,7 +28,8 @@ export const MenuItemSubList = styled(ListItem)(() => ({ })); export const IconWrapper = styled('div')({ - marginRight: '0.75rem' + marginRight: '0.75rem', + marginLeft: '0.5rem' }); export const SubIconWrapper = styled('div')({ diff --git a/src/custom/Prompt/promt-component.tsx b/src/custom/Prompt/promt-component.tsx index 871303671..44022d730 100644 --- a/src/custom/Prompt/promt-component.tsx +++ b/src/custom/Prompt/promt-component.tsx @@ -1,6 +1,6 @@ -import { useTheme } from '@mui/material'; import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { Typography } from '../../base'; +import { useTheme } from '../../theme'; import { Modal, ModalBody, ModalButtonPrimary, ModalButtonSecondary, ModalFooter } from '../Modal'; import { ActionComponent, Subtitle } from './style'; @@ -35,7 +35,7 @@ interface ShowParams { showInfoIcon?: string; } -interface PromptRef { +export interface PromptRef { show: (params: ShowParams) => Promise; } diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 9216e5bd4..aa1c51aac 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -17,7 +17,8 @@ export const IconWrapper = styled('div')<{ disabled?: boolean }>(({ disabled = f export const DataTableEllipsisMenu: React.FC<{ actionsList: NonNullable['actionsList']; -}> = ({ actionsList }) => { + theme?: Theme; +}> = ({ actionsList, theme }) => { const [anchorEl, setAnchorEl] = React.useState(null); const [isSocialShareOpen, setIsSocialShareOpen] = React.useState(false); @@ -43,13 +44,59 @@ export const DataTableEllipsisMenu: React.FC<{ return ( <> - } arrow /> - + } + arrow + /> + {actionsList && - actionsList.map((action, index) => ( - - {action.type === 'share-social' ? ( - <> + actionsList.map((action, index) => { + if (action.type === 'share-social') { + return [ + handleActionClick(action)} + disabled={action.disabled} + > + + + + + {action.title} + + , + + {action.customComponent} + + ]; + } else { + return ( + handleActionClick(action)} disabled={action.disabled} > - - - - {action.title} + {action.icon} + + {action.title} + - - {action.customComponent} - - > - ) : ( - <> - - handleActionClick(action)} - disabled={action.disabled} - > - {action.icon} - {action.title} - - - > - )} - - ))} + + ); + } + })} > ); }; -const dataTableTheme = (theme: Theme) => +const dataTableTheme = (theme: Theme, backgroundColor?: string) => createTheme({ components: { + MUIDataTable: { + styleOverrides: { + paper: { + background: backgroundColor || theme.palette.background.default, + maxWidth: '-moz-available' + } + } + }, MuiTable: { styleOverrides: { root: { @@ -103,7 +138,7 @@ const dataTableTheme = (theme: Theme) => '@media (max-width: 500px)': { wordWrap: 'break-word' }, - background: theme.palette.background.constant?.table, + background: backgroundColor || theme.palette.background.constant?.table, color: theme.palette.text.default } } @@ -118,7 +153,8 @@ const dataTableTheme = (theme: Theme) => root: { fontWeight: 'bold', textTransform: 'uppercase', - color: theme.palette.text.default + color: theme.palette.text.default, + backgroundColor: backgroundColor || theme.palette.background.constant?.table } } }, @@ -187,7 +223,7 @@ const dataTableTheme = (theme: Theme) => MUIDataTableSelectCell: { styleOverrides: { headerCell: { - background: theme.palette.background.constant?.table + background: backgroundColor || theme.palette.background.constant?.table } } }, @@ -249,8 +285,8 @@ export interface ResponsiveDataTableProps { theme?: object; colViews?: Record | undefined; rowsPerPageOptions?: number[] | undefined; + backgroundColor?: string; } - const ResponsiveDataTable = ({ data, columns, @@ -258,7 +294,9 @@ const ResponsiveDataTable = ({ tableCols, updateCols, columnVisibility, - rowsPerPageOptions = [10, 25, 50, 100], // Default and standard page size options + rowsPerPageOptions = [10, 25, 50, 100], + theme, + backgroundColor, ...props }: ResponsiveDataTableProps): JSX.Element => { const formatDate = (date: Date): string => { @@ -347,7 +385,6 @@ const ResponsiveDataTable = ({ } }); updateCols && updateCols([...columns]); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [columnVisibility, updateCols]); @@ -360,8 +397,16 @@ const ResponsiveDataTable = ({ Checkbox: Checkbox }; + const finalTheme = (baseTheme: Theme) => { + const defaultTheme = dataTableTheme(baseTheme, backgroundColor); + if (theme) { + return createTheme(defaultTheme, typeof theme === 'function' ? theme(baseTheme) : theme); + } + return defaultTheme; + }; + return ( - + diff --git a/src/custom/Stepper/index.tsx b/src/custom/Stepper/index.tsx index e993341db..5fd4107f5 100644 --- a/src/custom/Stepper/index.tsx +++ b/src/custom/Stepper/index.tsx @@ -1,9 +1,10 @@ -import { Box, Stack, Step, StepConnector, StepLabel, Stepper, useTheme } from '@mui/material'; +import { Box, Stack, Step, StepConnector, StepLabel, Stepper } from '@mui/material'; import { stepConnectorClasses } from '@mui/material/StepConnector'; import { StepIconProps } from '@mui/material/StepIcon'; import { styled } from '@mui/system'; import React, { useMemo, useState } from 'react'; import { IconProps } from '../../icons/types'; +import { useTheme } from '../../theme'; interface ColorlibStepIconPropsI extends StepIconProps { icons: React.ComponentType[]; diff --git a/src/custom/StyledSearchBar/StyledSearchBar.tsx b/src/custom/StyledSearchBar/StyledSearchBar.tsx index d543bf520..b608789c7 100644 --- a/src/custom/StyledSearchBar/StyledSearchBar.tsx +++ b/src/custom/StyledSearchBar/StyledSearchBar.tsx @@ -1,51 +1,106 @@ -import React from 'react'; -import { Box } from '../../base/Box'; -import { InputAdornment } from '../../base/Input'; -import { TextField } from '../../base/TextField'; +import { SxProps, Theme } from '@mui/material'; +import { debounce } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { InputAdornment } from '../../base'; +import { SearchIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { InputAdornmentEnd, StyledSearchInput } from './style'; interface SearchBarProps { onChange?: (event: React.ChangeEvent) => void; value?: string; width?: string; - label: string; + label?: string; + placeholder?: string; + sx?: SxProps; endAdornment?: React.ReactNode; + debounceTime?: number; } +/** + * StyledSearchBar component renders a search input field with customizable properties. + * + * @param {Object} props - The component props. + * @param {function} [props.onChange] - Function to handle the change event when the search input value changes. + * @param {string} [props.value] - The current value of the search input. + * @param {string} [props.label] - The label for the search input. + * @param {string} [props.placeholder] - The placeholder text for the search input. + * @param {Object} [props.sx] - The style object for the search input. + * @param {React.ReactNode} [props.endAdornment] - The element to display at the end of the search input. + * @param {number} [props.debounceTime] - The debounce time for the input change handler. + * + * @returns {JSX.Element} The rendered StyledSearchBar component. + */ function StyledSearchBar({ onChange, - value, - width, + value = '', label, + sx, + placeholder, endAdornment, - ...props + debounceTime = 300 }: SearchBarProps): JSX.Element { + const theme = useTheme(); + const [inputValue, setInputValue] = useState(value); + + // Update local state when controlled value changes + useEffect(() => { + if (value !== inputValue) { + setInputValue(value); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); + + // Create synthetic event helper + const createSyntheticEvent = (value: string): React.ChangeEvent => + ({ + target: { value }, + persist: () => {} + }) as React.ChangeEvent; + + // Memoize the debounced handler + const debouncedOnChange = useMemo( + () => + debounce((newValue: string) => { + onChange?.(createSyntheticEvent(newValue)); + }, debounceTime), + [onChange, debounceTime] + ); + + useEffect(() => { + if (!onChange) return; + if (inputValue === '') { + onChange(createSyntheticEvent(inputValue)); + } else { + debouncedOnChange(inputValue); + } + + return () => { + debouncedOnChange.cancel(); + }; + }, [inputValue, onChange, debouncedOnChange]); + + const handleChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + setInputValue(newValue); + }; + return ( - - :not(style)': { width } - }} - {...props} - > - {endAdornment} - }} - /> - - + + + + } + endAdornment={{endAdornment}} + /> ); } diff --git a/src/custom/StyledSearchBar/style.tsx b/src/custom/StyledSearchBar/style.tsx new file mode 100644 index 000000000..9f7026721 --- /dev/null +++ b/src/custom/StyledSearchBar/style.tsx @@ -0,0 +1,23 @@ +import { styled } from '@mui/material'; +import { InputAdornment, OutlinedInput } from '../../base'; + +export const StyledSearchInput = styled(OutlinedInput)(({ style, theme }) => ({ + width: '100%', + '@media (max-width: 590px)': { + marginLeft: '0.25rem', + paddingLeft: '0.25rem' + }, + display: 'flex', + backgroundColor: + theme.palette.mode === 'light' ? theme.palette.common.white : theme.palette.background.surfaces, + ...style +})); + +export const InputAdornmentEnd = styled(InputAdornment)(({ theme }) => ({ + borderLeft: `1px solid ${theme.palette.background.tertiary}`, + height: '30px', + paddingLeft: '10px', + '@media (max-width: 590px)': { + paddingLeft: '0px' + } +})); diff --git a/src/custom/Typography/index.tsx b/src/custom/Typography/index.tsx index 2f03bb473..011112891 100644 --- a/src/custom/Typography/index.tsx +++ b/src/custom/Typography/index.tsx @@ -140,7 +140,7 @@ export const ContentDetailsPoints = styled(TextB3Regular)(({ theme }) => ({ export const ContentDetailsText = styled(TextB1Regular)(({ theme }) => ({ ...commonTypographyStyles(theme), ['@media (min-width:1200px)']: { - fontSize: '1.3rem' + fontSize: '1' } })); diff --git a/src/custom/UniversalFilter.tsx b/src/custom/UniversalFilter.tsx index 1df8dbabd..02a8213fd 100644 --- a/src/custom/UniversalFilter.tsx +++ b/src/custom/UniversalFilter.tsx @@ -1,4 +1,3 @@ -import { useTheme } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; import React from 'react'; import { Button } from '../base/Button'; @@ -8,6 +7,7 @@ import { MenuItem } from '../base/MenuItem'; import { Paper } from '../base/Paper'; import { Select } from '../base/Select'; import { FilterIcon } from '../icons'; +import { useTheme } from '../theme'; import PopperListener from './PopperListener'; import TooltipIcon from './TooltipIcon'; diff --git a/src/custom/UserSearchField/UserSearchFieldInput.tsx b/src/custom/UserSearchField/UserSearchFieldInput.tsx new file mode 100644 index 000000000..c2f79b076 --- /dev/null +++ b/src/custom/UserSearchField/UserSearchFieldInput.tsx @@ -0,0 +1,320 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { Autocomplete } from '@mui/material'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + Avatar, + Box, + Checkbox, + Chip, + CircularProgress, + FormControlLabel, + FormGroup, + Grid, + TextField, + Tooltip, + Typography +} from '../../base'; +import { iconSmall } from '../../constants/iconsSizes'; +import { CloseIcon, PersonIcon } from '../../icons'; + +interface User { + id: string; + first_name: string; + last_name: string; + email: string; + avatar_url?: string; + deleted_at?: { Valid: boolean }; + deleted?: boolean; +} + +interface UserSearchFieldProps { + usersData: User[]; + setUsersData: React.Dispatch>; + label?: string; + setDisableSave?: (disable: boolean) => void; + handleNotifyPref?: () => void; + notifyUpdate?: boolean; + isCreate?: boolean; + searchType?: string; + disabled?: boolean; + currentUserData: User | null; + searchedUsers: User[]; + isUserSearchLoading: boolean; + fetchSearchedUsers: (value: string) => void; + usersSearch: string; + setUsersSearch: React.Dispatch>; +} + +const UserSearchField: React.FC = ({ + usersData, + setUsersData, + label = 'Add User', + setDisableSave, + handleNotifyPref, + notifyUpdate, + isCreate, + searchType, + disabled = false, + currentUserData, + searchedUsers = [], + isUserSearchLoading, + fetchSearchedUsers, + usersSearch, + setUsersSearch +}) => { + const [error, setError] = useState(''); + const [open, setOpen] = useState(false); + const [showAllUsers, setShowAllUsers] = useState(false); + const [hasInitialFocus, setHasInitialFocus] = useState(true); + const [inputValue, setInputValue] = useState(''); + const [localUsersData, setLocalUsersData] = useState(usersData || []); + + useEffect(() => { + setLocalUsersData(usersData || []); + }, [usersData]); + + const displayOptions = useMemo(() => { + if (hasInitialFocus && !usersSearch && currentUserData) { + return [currentUserData]; + } + + const filteredResults = searchedUsers.filter( + (user: User) => + user.id !== currentUserData?.id && + !localUsersData.some((selectedUser) => selectedUser.id === user.id) && + !user.deleted_at?.Valid + ); + + if (!usersSearch && currentUserData) { + return [currentUserData, ...filteredResults]; + } + + return filteredResults; + }, [searchedUsers, currentUserData, usersSearch, hasInitialFocus, localUsersData]); + + const handleDelete = useCallback( + (idToDelete: string, event: React.MouseEvent) => { + event.stopPropagation(); + + const updatedUsers = localUsersData.filter((user) => user.id !== idToDelete); + setLocalUsersData(updatedUsers); + setUsersData(updatedUsers); + + if (setDisableSave) { + setDisableSave(false); + } + }, + [localUsersData, setUsersData, setDisableSave, fetchSearchedUsers, inputValue] + ); + + const handleAdd = useCallback( + (event: React.SyntheticEvent, value: User | null) => { + if (!value) return; + + const isDuplicate = localUsersData.some((user) => user.id === value.id); + const isDeleted = value.deleted_at?.Valid === true; + + if (isDuplicate || isDeleted) { + setError(isDuplicate ? 'User already selected' : 'User does not exist'); + return; + } + setInputValue(''); + setUsersSearch(''); + setError(''); + setOpen(false); + + setLocalUsersData((prev) => [...prev, value]); + setUsersData((prev) => [...prev, value]); + + if (setDisableSave) { + setDisableSave(false); + } + }, + [localUsersData, setUsersData, setDisableSave, setUsersSearch] + ); + + const handleInputChange = useCallback( + (event: React.SyntheticEvent, newValue: string) => { + setInputValue(newValue); + + if (newValue === '') { + setOpen(true); + setUsersSearch(''); + setHasInitialFocus(true); + } else { + const encodedValue = encodeURIComponent(newValue); + fetchSearchedUsers(encodedValue); + setError(''); + setOpen(true); + setHasInitialFocus(false); + } + }, + [fetchSearchedUsers] + ); + + return ( + <> + inputValue} + isOptionEqualToValue={(option, value) => option.id === value.id} + onOpen={() => setOpen(true)} + onClose={() => setOpen(false)} + inputValue={inputValue} + onChange={handleAdd} + onInputChange={handleInputChange} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + filterOptions={(options, { inputValue }) => { + return options.filter((option: User) => { + const searchStr = inputValue.toLowerCase(); + return ( + option.first_name?.toLowerCase().includes(searchStr) || + option.last_name?.toLowerCase().includes(searchStr) || + option.email?.toLowerCase().includes(searchStr) + ); + }); + }} + loading={isUserSearchLoading} + disabled={disabled} + disableClearable + value={undefined} + selectOnFocus={false} + blurOnSelect={true} + clearOnBlur={true} + popupIcon={null} + forcePopupIcon={false} + noOptionsText={isUserSearchLoading ? 'Loading...' : 'No users found'} + renderInput={(params) => ( + + {isUserSearchLoading ? : null} + + ) + }} + /> + )} + renderOption={(props, option: User) => ( + + img': { mr: 2, flexShrink: 0 } }}> + {' '} + + + + + {option.avatar_url ? '' : } + + + + + {option.deleted ? ( + + {option.email} (deleted) + + ) : ( + <> + + {option.first_name} {option.last_name} + + + {option.email} + + > + )} + + + + + )} + /> + + {!isCreate && ( + + + + } + label={`Notify ${searchType} of membership change`} + /> + + + )} + 0 ? '0.5rem' : '' + }} + > + {!showAllUsers && localUsersData?.[0] && ( + + {!localUsersData[0].avatar_url && localUsersData[0].first_name?.[0]} + + } + label={localUsersData[0].email} + onDelete={(e) => handleDelete(localUsersData[0].id, e)} + deleteIcon={ + + + + } + size="small" + /> + )} + + {showAllUsers && + localUsersData?.map((user) => ( + + {!user.avatar_url && user.first_name?.[0]} + + } + label={user.email} + onDelete={(e) => handleDelete(user.id, e)} + deleteIcon={ + + + + } + size="small" + /> + ))} + + {localUsersData?.length > 1 && ( + setShowAllUsers(!showAllUsers)} + sx={{ + cursor: 'pointer' + }} + > + {showAllUsers ? '(hide)' : `(+${localUsersData.length - 1})`} + + )} + + > + ); +}; + +export default UserSearchField; diff --git a/src/custom/UserSearchField/index.ts b/src/custom/UserSearchField/index.ts new file mode 100644 index 000000000..7cf9a5ea5 --- /dev/null +++ b/src/custom/UserSearchField/index.ts @@ -0,0 +1,3 @@ +import UserSearchField from './UserSearchFieldInput'; + +export { UserSearchField }; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 8ec34c8a4..1918d1759 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -3,6 +3,7 @@ import { BookmarkNotification } from './BookmarkNotification'; import CatalogFilter, { CatalogFilterProps } from './CatalogFilter/CatalogFilter'; import { ChapterCard } from './ChapterCard'; import { ConnectionChip } from './ConnectionChip'; +import { CatalogCardDesignLogo, CustomCatalogCard, EmptyStateCard } from './CustomCatalog'; import { CustomColumn, CustomColumnVisibilityControl, @@ -30,7 +31,7 @@ import { useWindowDimensions } from './Helpers/Dimension'; import { useNotificationHandler } from './Helpers/Notification'; import { ColView, updateVisibleColumns } from './Helpers/ResponsiveColumns/responsive-coulmns.tsx'; import { LearningCard } from './LearningCard'; -import { RenderMarkdown } from './Markdown'; +import { BasicMarkdown, RenderMarkdown } from './Markdown'; import { ModalCard } from './ModalCard'; import PopperListener, { IPopperListener } from './PopperListener'; import PromptComponent from './Prompt'; @@ -43,7 +44,10 @@ import { TransferList } from './TransferModal/TransferList'; import { TransferListProps } from './TransferModal/TransferList/TransferList'; import UniversalFilter, { UniversalFilterProps } from './UniversalFilter'; export { CatalogCard } from './CatalogCard'; +export { CatalogFilterSidebar } from './CatalogFilterSection'; +export type { FilterListType } from './CatalogFilterSection'; export { StyledChartDialog } from './ChartDialog'; +export { InputSearchField } from './InputSearchField'; export { LearningContent } from './LearningContent'; export { NavigationNavbar } from './NavigationNavbar'; export { Note } from './Note'; @@ -53,18 +57,22 @@ export { StyledSearchBar } from './StyledSearchBar'; export { TOC } from './TOCChapter'; export { TOCLearning } from './TOCLearning'; export { Terminal } from './Terminal'; +export { UserSearchField } from './UserSearchField'; export { ActionButton, BookmarkNotification, + CatalogCardDesignLogo, CatalogFilter, ChapterCard, ConnectionChip, + CustomCatalogCard, CustomColumnVisibilityControl, CustomDialog, CustomImage, CustomTooltip, DataTableEllipsisMenu, EmptyState, + EmptyStateCard, ErrorBoundary, Fallback, FeedbackButton, @@ -105,7 +113,7 @@ export { // Markdown export { StyledMarkdown } from './Markdown/style'; -export { RenderMarkdown }; +export { BasicMarkdown, RenderMarkdown }; // Stepper export { CustomizedStepper, useStepper } from './Stepper'; @@ -124,5 +132,7 @@ export type { UniversalFilterProps }; +export * from './CatalogDesignTable'; +export * from './CatalogDetail'; export * from './Dialog'; export * from './permissions'; diff --git a/src/icons/CatalogIcon/CatalogIcon.tsx b/src/icons/CatalogIcon/CatalogIcon.tsx index 6129d27a2..12771597e 100644 --- a/src/icons/CatalogIcon/CatalogIcon.tsx +++ b/src/icons/CatalogIcon/CatalogIcon.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; -import { CARIBBEAN_GREEN, DARK_SLATE_GRAY, KEPPEL, WHITE, useTheme } from '../../theme'; +import { CARIBBEAN_GREEN, DARK_SLATE_GRAY, KEPPEL, useTheme } from '../../theme'; import { IconProps } from '../types'; type CatalogIconProps = { @@ -14,13 +14,11 @@ export const CatalogIcon: FC = ({ height = DEFAULT_HEIGHT, primaryFill, secondaryFill, - tertiaryFill = WHITE, style = {}, ...props }) => { const theme = useTheme(); const themeMode = theme?.palette?.mode ?? 'light'; - const themePrimaryFill = primaryFill ?? (themeMode === 'dark' ? KEPPEL : DARK_SLATE_GRAY); const themeSecondaryFill = secondaryFill ?? (themeMode === 'dark' ? CARIBBEAN_GREEN : KEPPEL); @@ -34,29 +32,24 @@ export const CatalogIcon: FC = ({ {...props} > + - ); diff --git a/src/icons/Chain/ChainIcon.tsx b/src/icons/Chain/ChainIcon.tsx new file mode 100644 index 000000000..727bfa386 --- /dev/null +++ b/src/icons/Chain/ChainIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface ChainIconProps { + width: string; + height: string; + fill?: string; + style?: React.CSSProperties; + secondaryFill?: string; +} + +const ChainIcon: React.FC = ({ width, height, style, fill = '#3C494F' }) => ( + + + +); + +export default ChainIcon; diff --git a/src/icons/Chain/index.ts b/src/icons/Chain/index.ts new file mode 100644 index 000000000..043710db5 --- /dev/null +++ b/src/icons/Chain/index.ts @@ -0,0 +1 @@ +export { default as ChainIcon } from './ChainIcon'; diff --git a/src/icons/Challenges/ChallengesIcon.tsx b/src/icons/Challenges/ChallengesIcon.tsx new file mode 100644 index 000000000..94b9b7b7a --- /dev/null +++ b/src/icons/Challenges/ChallengesIcon.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +interface ChallengesIconProps { + width?: string; + height?: string; + primaryFill?: string; + brandFill?: string; + secondaryFill?: string; + style?: React.CSSProperties; +} + +const ChallengesIcon: React.FC = ({ + width = '32px', + height = '32px', + primaryFill = '#B1B9BC', + brandFill = '#00B39F', + secondaryFill = '#51636B', + style = {} +}) => ( + + + + + + + + + + +); + +export default ChallengesIcon; diff --git a/src/icons/Challenges/index.ts b/src/icons/Challenges/index.ts new file mode 100644 index 000000000..e9cf9c6d5 --- /dev/null +++ b/src/icons/Challenges/index.ts @@ -0,0 +1 @@ +export { default as ChallengesIcon } from './ChallengesIcon'; diff --git a/src/icons/CollapseAll/CollapseAllIcon.tsx b/src/icons/CollapseAll/CollapseAllIcon.tsx new file mode 100644 index 000000000..66afdc313 --- /dev/null +++ b/src/icons/CollapseAll/CollapseAllIcon.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +interface CollapsAllIconProps { + height?: string; + width?: string; + fill?: string; + strokeWidth?: string; + style?: React.CSSProperties; +} + +const CollapsAllIcon: React.FC = ({ + height = '24', + width = '24', + fill = 'none', + strokeWidth = '2', + style +}) => ( + + + + +); + +export default CollapsAllIcon; diff --git a/src/icons/CollapseAll/index.tsx b/src/icons/CollapseAll/index.tsx new file mode 100644 index 000000000..dd1fa97d4 --- /dev/null +++ b/src/icons/CollapseAll/index.tsx @@ -0,0 +1 @@ +export { default as CollapseAllIcon } from './CollapseAllIcon'; diff --git a/src/icons/ContentClassIcons/CommunityClassIcon.tsx b/src/icons/ContentClassIcons/CommunityClassIcon.tsx index ad000fc1e..393c609bd 100644 --- a/src/icons/ContentClassIcons/CommunityClassIcon.tsx +++ b/src/icons/ContentClassIcons/CommunityClassIcon.tsx @@ -3,8 +3,8 @@ import { CustomIconProps } from '../types'; export const CommunityClassIcon: FC = ({ width = '16', height = '13', - secondaryFill = '#293B43', - primaryFill = '#647176', + fill = '#293B43', + secondaryFill = '#647176', style = {} }) => ( = ({ > ); diff --git a/src/icons/Copy/CopyIcon.tsx b/src/icons/Copy/CopyIcon.tsx new file mode 100644 index 000000000..89424292c --- /dev/null +++ b/src/icons/Copy/CopyIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +interface CopyIconProps { + width: number; + height: number; + fill?: string; + style?: React.CSSProperties; + secondaryFill?: string; +} + +const CopyIcon: React.FC = ({ width, height, fill = 'white', style }) => ( + + + +); + +export default CopyIcon; diff --git a/src/icons/Copy/index.ts b/src/icons/Copy/index.ts index 48fc57203..cd7aaba8a 100644 --- a/src/icons/Copy/index.ts +++ b/src/icons/Copy/index.ts @@ -1 +1,2 @@ +export { default as CopyIcon } from './CopyIcon'; export { default as CopyLinkIcon } from './CopyLinkIcon'; diff --git a/src/icons/EmptyStyle/EmptyStyleIcon.tsx b/src/icons/EmptyStyle/EmptyStyleIcon.tsx new file mode 100644 index 000000000..dcd90aae1 --- /dev/null +++ b/src/icons/EmptyStyle/EmptyStyleIcon.tsx @@ -0,0 +1,31 @@ +import { CSSProperties, FC } from 'react'; + +interface EmptyStyleIconProps { + width?: string; + height?: string; + fill?: string; + style?: CSSProperties; + onClick?: () => void; +} + +const EmptyStyleIcon: FC = ({ + width = '24px', + height = '24px', + fill, + style = {}, + onClick = () => {} +}) => ( + + + +); + +export default EmptyStyleIcon; diff --git a/src/icons/EmptyStyle/index.tsx b/src/icons/EmptyStyle/index.tsx new file mode 100644 index 000000000..97ba77dd5 --- /dev/null +++ b/src/icons/EmptyStyle/index.tsx @@ -0,0 +1 @@ +export { default as EmptyStyleIcon } from './EmptyStyleIcon'; diff --git a/src/icons/ExpandAll/ExpandAllIcon.tsx b/src/icons/ExpandAll/ExpandAllIcon.tsx new file mode 100644 index 000000000..d9e790048 --- /dev/null +++ b/src/icons/ExpandAll/ExpandAllIcon.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +interface ExpandAllIconProps { + height?: string; + width?: string; + fill?: string; + strokeWidth?: string; + style?: React.CSSProperties; +} + +const ExpandAllIcon: React.FC = ({ + height = '24', + width = '24', + fill = 'none', + strokeWidth = '2', + style +}) => ( + + + + +); + +export default ExpandAllIcon; diff --git a/src/icons/ExpandAll/index.tsx b/src/icons/ExpandAll/index.tsx new file mode 100644 index 000000000..0f9a8c8f9 --- /dev/null +++ b/src/icons/ExpandAll/index.tsx @@ -0,0 +1 @@ +export { default as ExpandAllIcon } from './ExpandAllIcon'; diff --git a/src/icons/GridView/GridViewIcon.tsx b/src/icons/GridView/GridViewIcon.tsx new file mode 100644 index 000000000..049d05f90 --- /dev/null +++ b/src/icons/GridView/GridViewIcon.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface GridViewIconProps { + width?: string; + height?: string; + fill?: string; + opacity?: number; + style?: React.CSSProperties; +} + +export const GridViewIcon: React.FC = ({ + width = '24', + height = '28.8', + fill, + opacity, + style = {} +}) => ( + + + +); + +export default GridViewIcon; diff --git a/src/icons/GridView/index.ts b/src/icons/GridView/index.ts new file mode 100644 index 000000000..43f10c663 --- /dev/null +++ b/src/icons/GridView/index.ts @@ -0,0 +1 @@ +export { default as GridViewIcon } from './GridViewIcon'; diff --git a/src/icons/Kanvas/KanvasIcon.tsx b/src/icons/Kanvas/KanvasIcon.tsx new file mode 100644 index 000000000..affe38816 --- /dev/null +++ b/src/icons/Kanvas/KanvasIcon.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +interface KanvasIconProps { + width: number; + height: number; + fill?: string; + style?: React.CSSProperties; + primaryFill?: string; + secondaryFill?: string; +} + +const KanvasIcon: React.FC = ({ + width, + height, + fill, + style, + primaryFill = 'white' +}) => ( + + + + + + + + + + + + + + +); + +export default KanvasIcon; diff --git a/src/icons/Kanvas/index.ts b/src/icons/Kanvas/index.ts new file mode 100644 index 000000000..3d2dcdd5c --- /dev/null +++ b/src/icons/Kanvas/index.ts @@ -0,0 +1 @@ +export { default as KanvasIcon } from './KanvasIcon'; diff --git a/src/icons/Learning/LearningIcon.tsx b/src/icons/Learning/LearningIcon.tsx new file mode 100644 index 000000000..cf485d448 --- /dev/null +++ b/src/icons/Learning/LearningIcon.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +interface LearningIconProps { + width?: string; + height?: string; + primaryFill?: string; + secondaryFill?: string; + style?: React.CSSProperties; +} + +const LearningIcon: React.FC = ({ + width = '32px', + height = '32px', + primaryFill = '#FDFDFD', + secondaryFill = '#FDFDFD', + style = {} +}) => ( + + + + +); + +export default LearningIcon; diff --git a/src/icons/Learning/index.ts b/src/icons/Learning/index.ts new file mode 100644 index 000000000..4849b25e4 --- /dev/null +++ b/src/icons/Learning/index.ts @@ -0,0 +1 @@ +export { default as LearningIcon } from './LearningIcon'; diff --git a/src/icons/Menu/MenuIcon.tsx b/src/icons/Menu/MenuIcon.tsx index 5a1fcc2e7..101ab13f8 100644 --- a/src/icons/Menu/MenuIcon.tsx +++ b/src/icons/Menu/MenuIcon.tsx @@ -1,7 +1,7 @@ import { DEFAULT_FILL_NONE, DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; import { IconProps } from '../types'; -export const AddIcon = ({ +export const MenuIcon = ({ width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT, fill = DEFAULT_FILL_NONE, @@ -13,7 +13,7 @@ export const AddIcon = ({ height={height} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" - data-testid="add-icon-svg" + data-testid="menu-icon-svg" {...props} > @@ -21,4 +21,4 @@ export const AddIcon = ({ ); }; -export default AddIcon; +export default MenuIcon; diff --git a/src/icons/MesheryFilter/MesheryFilterIcon.tsx b/src/icons/MesheryFilter/MesheryFilterIcon.tsx new file mode 100644 index 000000000..ac9ee4da4 --- /dev/null +++ b/src/icons/MesheryFilter/MesheryFilterIcon.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react'; +import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../../constants/constants'; +import { CustomIconProps } from '../types'; + +const MesheryFilterIcon: FC = ({ + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT, + primaryFill = '#654ff0', + secondaryFill = '#ffffff', + ...props +}) => { + return ( + + + + + ); +}; + +export default MesheryFilterIcon; diff --git a/src/icons/MesheryFilter/index.tsx b/src/icons/MesheryFilter/index.tsx new file mode 100644 index 000000000..c6a3c59e4 --- /dev/null +++ b/src/icons/MesheryFilter/index.tsx @@ -0,0 +1 @@ +export { default as MesheryFilterIcon } from './MesheryFilterIcon'; diff --git a/src/icons/Organization/OrgIcon.tsx b/src/icons/Organization/OrgIcon.tsx new file mode 100644 index 000000000..94ea08d68 --- /dev/null +++ b/src/icons/Organization/OrgIcon.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +interface OrgIconProps { + width?: number; + height?: number; + fill?: string; + secondaryFill?: string; +} + +const OrgIcon: React.FC = ({ + width = 24, + height = 24, + fill = '#F6F8F8', + secondaryFill = '#294957' +}) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export default OrgIcon; diff --git a/src/icons/Organization/index.ts b/src/icons/Organization/index.ts new file mode 100644 index 000000000..064cfa999 --- /dev/null +++ b/src/icons/Organization/index.ts @@ -0,0 +1,2 @@ +import OrgIcon from './OrgIcon'; +export { OrgIcon }; diff --git a/src/icons/Person/PersonIcon.tsx b/src/icons/Person/PersonIcon.tsx new file mode 100644 index 000000000..fcb81007c --- /dev/null +++ b/src/icons/Person/PersonIcon.tsx @@ -0,0 +1,21 @@ +const PersonIcon = ({ + width = '24px', + height = '24px', + fill = 'currentColor', + style = {}, + onClick = () => {} +}) => ( + + + +); + +export default PersonIcon; diff --git a/src/icons/Person/index.ts b/src/icons/Person/index.ts new file mode 100644 index 000000000..c31dfbfe5 --- /dev/null +++ b/src/icons/Person/index.ts @@ -0,0 +1 @@ +export { default as PersonIcon } from './PersonIcon'; diff --git a/src/icons/Share/ShareLineIcon.tsx b/src/icons/Share/ShareLineIcon.tsx new file mode 100644 index 000000000..6a4c6fd3a --- /dev/null +++ b/src/icons/Share/ShareLineIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +interface ShareLineIconProps { + width: string; + height: string; + fill?: string; + style?: React.CSSProperties; + secondaryFill?: string; +} + +const ShareLineIcon: React.FC = ({ + width, + height, + style, + fill = '#3C494F' +}) => ( + + + +); + +export default ShareLineIcon; diff --git a/src/icons/Share/index.tsx b/src/icons/Share/index.tsx index 182307123..5abf944db 100644 --- a/src/icons/Share/index.tsx +++ b/src/icons/Share/index.tsx @@ -1,2 +1,3 @@ import ShareIcon from './ShareIcon'; -export { ShareIcon }; +import ShareLineIcon from './ShareLineIcon'; +export { ShareIcon, ShareLineIcon }; diff --git a/src/icons/SocialMedial/FacebookIcon.tsx b/src/icons/SocialMedial/FacebookIcon.tsx new file mode 100644 index 000000000..0fcf1ea65 --- /dev/null +++ b/src/icons/SocialMedial/FacebookIcon.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { IconProps } from './types'; + +const FacebookIcon: React.FC = ({ width = 40, height = 40 }) => ( + + + + + + + + + + + + + + + + + +); + +export default FacebookIcon; diff --git a/src/icons/SocialMedial/LinkedinIcon.tsx b/src/icons/SocialMedial/LinkedinIcon.tsx new file mode 100644 index 000000000..5ca0d4948 --- /dev/null +++ b/src/icons/SocialMedial/LinkedinIcon.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { IconProps } from './types'; + +const LinkedinIcon: React.FC = ({ width = 40, height = 40 }) => ( + + + + + + + + + + + + + + + + + +); + +export default LinkedinIcon; diff --git a/src/icons/SocialMedial/TwitterIcon.tsx b/src/icons/SocialMedial/TwitterIcon.tsx new file mode 100644 index 000000000..df8172d42 --- /dev/null +++ b/src/icons/SocialMedial/TwitterIcon.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { IconProps } from './types'; + +const TwitterIcon: React.FC = ({ width = 40, height = 40 }) => ( + + + + + + + + + + + + + + + + + +); + +export default TwitterIcon; diff --git a/src/icons/SocialMedial/index.ts b/src/icons/SocialMedial/index.ts new file mode 100644 index 000000000..1b3be9b14 --- /dev/null +++ b/src/icons/SocialMedial/index.ts @@ -0,0 +1,5 @@ +import FacebookIcon from './FacebookIcon'; +import LinkedinIcon from './LinkedinIcon'; +import TwitterIcon from './TwitterIcon'; + +export { FacebookIcon, LinkedinIcon, TwitterIcon }; diff --git a/src/icons/SocialMedial/types.ts b/src/icons/SocialMedial/types.ts new file mode 100644 index 000000000..f1505e285 --- /dev/null +++ b/src/icons/SocialMedial/types.ts @@ -0,0 +1,6 @@ +export interface IconProps { + width?: number; + height?: number; + fill?: string; + style?: React.CSSProperties; +} diff --git a/src/icons/TableView/TableViewIcon.tsx b/src/icons/TableView/TableViewIcon.tsx new file mode 100644 index 000000000..6bd33018d --- /dev/null +++ b/src/icons/TableView/TableViewIcon.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface TableViewIconProps { + width?: string; + height?: string; + fill?: string; + opacity?: number; + style?: React.CSSProperties; +} + +export const TableViewIcon: React.FC = ({ + width = '24', + height = '28.8', + fill, + opacity, + style = {} +}) => ( + + + +); + +export default TableViewIcon; diff --git a/src/icons/TableView/index.ts b/src/icons/TableView/index.ts new file mode 100644 index 000000000..742c66580 --- /dev/null +++ b/src/icons/TableView/index.ts @@ -0,0 +1 @@ +export { default as TableViewIcon } from './TableViewIcon'; diff --git a/src/icons/Workspace/WorkspaceIcon.tsx b/src/icons/Workspace/WorkspaceIcon.tsx index 8b6966356..9695cb33d 100644 --- a/src/icons/Workspace/WorkspaceIcon.tsx +++ b/src/icons/Workspace/WorkspaceIcon.tsx @@ -1,11 +1,13 @@ import { DEFAULT_HEIGHT, DEFAULT_WIDTH, KEPPEL_GREEN_FILL } from '../../constants/constants'; -import { IconProps } from '../types'; +import { CustomIconProps } from '../types'; export const WorkspaceIcon = ({ width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT, + secondaryFill = KEPPEL_GREEN_FILL, + opacity = 0.8, ...props -}: IconProps): JSX.Element => { +}: CustomIconProps): JSX.Element => { return ( ); diff --git a/src/icons/index.ts b/src/icons/index.ts index 6a8b44302..22a3a7217 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -9,6 +9,7 @@ export * from './Circle'; export * from './Clone'; export * from './Close'; export * from './Cloud'; +export * from './CollapseAll'; export * from './Column'; export * from './Component'; export * from './Configuration'; @@ -25,6 +26,7 @@ export * from './Designer'; export * from './Detail'; export * from './DropDownIcon'; export * from './Error'; +export * from './ExpandAll'; export * from './Favorite'; export * from './Filter'; export * from './Fullscreen'; @@ -38,6 +40,8 @@ export * from './Mesh'; // export { default as ModifiedApplicationFileIcon } from "./ModifiedApplicationFileIcon"; // export { default as OriginalApplicationFileIcon } from "./OriginalApplicationFileIcon"; export * from './Calender'; +export * from './Chain'; +export * from './Challenges'; export * from './ChevronLeft'; export * from './ContentClassIcons'; export * from './Deployments'; @@ -45,20 +49,27 @@ export * from './Design'; export * from './Done'; export * from './Download'; export * from './Edit'; +export * from './EmptyStyle'; export * from './Environment'; export * from './ExternalLink'; export * from './Feedback'; +export * from './GridView'; export * from './HelpIcon'; export * from './Idea'; export * from './InfoOutlined'; +export * from './Kanvas'; export * from './Kubernetes'; +export * from './Learning'; export * from './LeftAngledArrow'; export * from './LeftArrow'; export * from './Menu'; +export * from './MesheryFilter'; export * from './MesheryOperator'; export * from './Open'; +export * from './Organization'; export * from './PanTool'; export * from './Pattern'; +export * from './Person'; export * from './Pod'; export * from './Publish'; export * from './Question'; @@ -75,8 +86,10 @@ export * from './Screenshot'; export * from './Search'; export * from './Settings'; export * from './Share'; +export * from './SocialMedial'; export * from './Star'; export * from './Success'; +export * from './TableView'; export * from './TerminalIcon'; export * from './Toolkit'; export * from './Touch'; diff --git a/src/schemas/publishCatalogItem/schema.tsx b/src/schemas/publishCatalogItem/schema.tsx index 9219fc2bc..06295112f 100644 --- a/src/schemas/publishCatalogItem/schema.tsx +++ b/src/schemas/publishCatalogItem/schema.tsx @@ -24,14 +24,16 @@ const publishCatalogItemSchema = { description: 'Specific stipulations to consider and known behaviors to be aware of when using this design.', format: 'textarea', - 'x-rjsf-grid-area': 12 + 'x-rjsf-grid-area': 12, + 'x-encode-in-uri': true }, pattern_info: { type: 'string', title: 'Description', description: 'Purpose of the design along with its intended and unintended uses.', format: 'textarea', - 'x-rjsf-grid-area': 12 + 'x-rjsf-grid-area': 12, + 'x-encode-in-uri': true }, type: { type: 'string', diff --git a/src/schemas/publishCatalogItem/uiSchema.tsx b/src/schemas/publishCatalogItem/uiSchema.tsx index 646098bdf..2e2e9f695 100644 --- a/src/schemas/publishCatalogItem/uiSchema.tsx +++ b/src/schemas/publishCatalogItem/uiSchema.tsx @@ -1,5 +1,5 @@ const publishCatalogItemUiSchema = { - 'ui:order': ['type', 'compatibility', 'pattern_caveats', 'pattern_info'] + 'ui:order': ['type', 'compatibility', 'pattern_info', 'pattern_caveats'] }; export default publishCatalogItemUiSchema; diff --git a/src/theme/colors/colors.ts b/src/theme/colors/colors.ts index 24a60e708..107bc4f36 100644 --- a/src/theme/colors/colors.ts +++ b/src/theme/colors/colors.ts @@ -7,6 +7,7 @@ export const KEPPEL = '#00B39F'; export const DARK_KEPPEL = '#00A18F'; export const CARIBBEAN_GREEN = '#00D3A9'; export const TEAL_BLUE = '#477E96'; +export const DARK_TEAL_BLUE = '#3B687B'; export const CHARCOAL = '#3C494F'; export const BLACK = '#000000'; export const MIDNIGHT_BLACK = '#111111'; @@ -324,6 +325,15 @@ export const buttonDelete = { hover: redDelete.light }; +export const darkModalGradient = { + header: `linear-gradient(90deg, ${charcoal[30]} 0%, ${accentGrey[30]} 100%)`, + fotter: `linear-gradient(90deg, ${accentGrey[30]} 0%, ${charcoal[30]} 100%)` +}; + +export const lightModalGradient = { + header: `linear-gradient(90deg, ${TEAL_BLUE} 0%, ${DARK_TEAL_BLUE} 100%)`, + fotter: `linear-gradient(90deg, ${DARK_TEAL_BLUE} 0%, ${TEAL_BLUE} 100%)` +}; /** * Notification Colors */ diff --git a/src/theme/palette.ts b/src/theme/palette.ts index 062246e03..cba6b52b4 100644 --- a/src/theme/palette.ts +++ b/src/theme/palette.ts @@ -307,7 +307,7 @@ export const lightModePalette: PaletteOptions = { export const darkModePalette: PaletteOptions = { background: { default: Colors.charcoal[10], - secondary: Colors.accentGrey[10], + secondary: Colors.accentGrey[20], tertiary: Colors.accentGrey[30], hover: Colors.charcoal[20], supplementary: Colors.accentGrey[40],
{(error as Error).message}
+ Error: + {(error as Error).message} +