├── .nvmrc ├── .npmrc ├── .eslintrc.json ├── app ├── icon.png ├── favicon-old.ico ├── [[...slug]] │ └── page.tsx └── layout.tsx ├── public ├── SAP-scrn-R.png ├── images │ ├── anna_schmidt.jpg │ ├── david_miller.jpg │ ├── emily_johnson.jpg │ ├── jonas_fischer.jpg │ ├── lukas_muller.jpg │ ├── sarah_anderson.jpg │ ├── powertoolz-icon-black.png │ ├── powertoolz-logo-black.png │ ├── powertoolz-logo-white.png │ └── powertoolz-icon-yellow.png ├── vercel.svg └── next.svg ├── postcss.config.js ├── .vscode └── settings.json ├── src ├── components │ ├── designSystem │ │ ├── navigation │ │ │ ├── index.ts │ │ │ ├── breadcrumbs.tsx │ │ │ ├── menu-item.tsx │ │ │ └── menu.tsx │ │ ├── products │ │ │ ├── index.ts │ │ │ └── product-list.tsx │ │ ├── data-table │ │ │ ├── index.tsx │ │ │ ├── table-head.tsx │ │ │ ├── table-title-bar.tsx │ │ │ ├── status-select.tsx │ │ │ └── table-body.tsx │ │ ├── shared │ │ │ ├── content-error.tsx │ │ │ ├── edit-text.tsx │ │ │ ├── grid-button.tsx │ │ │ ├── sorts.tsx │ │ │ ├── loading.tsx │ │ │ └── pagination.tsx │ │ ├── orders │ │ │ ├── order-details-status.tsx │ │ │ ├── order-details-modal.tsx │ │ │ ├── order-details-header.tsx │ │ │ ├── order-details-footer.tsx │ │ │ └── order-details-entries.tsx │ │ ├── quotes │ │ │ ├── quote-details-status.tsx │ │ │ ├── quote-details-modal.tsx │ │ │ ├── quote-details-header.tsx │ │ │ ├── quote-details-footer.tsx │ │ │ └── quote-details-entries.tsx │ │ ├── content │ │ │ ├── heading.tsx │ │ │ ├── avatar.tsx │ │ │ ├── alert.tsx │ │ │ ├── rating.tsx │ │ │ ├── products │ │ │ │ └── product-carousel.tsx │ │ │ ├── button.tsx │ │ │ ├── widgets │ │ │ │ └── info-widget.tsx │ │ │ ├── faqs │ │ │ │ ├── faq-list.tsx │ │ │ │ └── faq.tsx │ │ │ ├── testimonial.tsx │ │ │ ├── hero.tsx │ │ │ ├── icon.tsx │ │ │ └── promo-card.tsx │ │ ├── tickets │ │ │ ├── ticket-details-header.tsx │ │ │ ├── ticket-details-modal.tsx │ │ │ └── ticket-details-events.tsx │ │ ├── search │ │ │ └── search-box.tsx │ │ ├── users │ │ │ ├── logout-modal.tsx │ │ │ ├── login-cards.tsx │ │ │ ├── profile-details-modal.tsx │ │ │ ├── login-card.tsx │ │ │ └── mini-profile.tsx │ │ ├── picker-options.tsx │ │ └── index.tsx │ ├── layout │ │ ├── footer-slot │ │ │ ├── copyright.tsx │ │ │ ├── index.tsx │ │ │ ├── social-nav.tsx │ │ │ └── footer-nav.tsx │ │ ├── header-slot │ │ │ ├── header-tools.tsx │ │ │ ├── site-logo.tsx │ │ │ ├── eyebrow-navigation.tsx │ │ │ ├── primary-nav.tsx │ │ │ ├── sign-in.tsx │ │ │ ├── locale-selector.tsx │ │ │ └── index.tsx │ │ └── index.ts │ ├── slug-rewriter.tsx │ └── studio-config.ts ├── utils │ ├── local-storage-utils.tsx │ ├── string-utils.tsx │ ├── table-utils.tsx │ ├── locale-utils.tsx │ ├── image-utils.tsx │ └── content-utils.tsx ├── mocks │ ├── currencies.tsx │ ├── languages.tsx │ ├── cost_centers.tsx │ ├── addresses.tsx │ ├── index.ts │ ├── b2b_units.tsx │ ├── delivery-modes.tsx │ ├── countries.tsx │ ├── users.tsx │ └── tickets.tsx ├── services │ ├── contentful │ │ └── client.ts │ └── sap │ │ ├── products.tsx │ │ └── sap-client.tsx ├── hooks │ ├── providers.tsx │ ├── site-labels-context.tsx │ ├── edit-mode-context.tsx │ ├── index.ts │ ├── site-config-context.tsx │ └── app-context.tsx ├── styles │ └── globals.css └── models │ └── content-types.tsx ├── .gitignore ├── next.config.mjs ├── tailwind.config.ts ├── .github └── workflows │ └── codeql.yml ├── tsconfig.json ├── package.json └── example.env /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.10.0 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/app/icon.png -------------------------------------------------------------------------------- /app/favicon-old.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/app/favicon-old.ico -------------------------------------------------------------------------------- /public/SAP-scrn-R.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/SAP-scrn-R.png -------------------------------------------------------------------------------- /public/images/anna_schmidt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/anna_schmidt.jpg -------------------------------------------------------------------------------- /public/images/david_miller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/david_miller.jpg -------------------------------------------------------------------------------- /public/images/emily_johnson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/emily_johnson.jpg -------------------------------------------------------------------------------- /public/images/jonas_fischer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/jonas_fischer.jpg -------------------------------------------------------------------------------- /public/images/lukas_muller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/lukas_muller.jpg -------------------------------------------------------------------------------- /public/images/sarah_anderson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/sarah_anderson.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/powertoolz-icon-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/powertoolz-icon-black.png -------------------------------------------------------------------------------- /public/images/powertoolz-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/powertoolz-logo-black.png -------------------------------------------------------------------------------- /public/images/powertoolz-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/powertoolz-logo-white.png -------------------------------------------------------------------------------- /public/images/powertoolz-icon-yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/b2b-demo/main/public/images/powertoolz-icon-yellow.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotenv.enableAutocloaking": false, 3 | "editor.formatOnSave": true, 4 | "prettier.jsxSingleQuote": true, 5 | "prettier.singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /src/components/designSystem/navigation/index.ts: -------------------------------------------------------------------------------- 1 | import Crumbs from './breadcrumbs'; 2 | import Menu from './menu'; 3 | export { breadcrumbsDefinition } from './breadcrumbs'; 4 | export { menuDefinition } from './menu'; 5 | export { Crumbs, Menu }; 6 | -------------------------------------------------------------------------------- /src/components/designSystem/products/index.ts: -------------------------------------------------------------------------------- 1 | import ProductCard from './product-card'; 2 | import ProductDetails from './product-details'; 3 | import ProductFacets from './product-facets'; 4 | import ProductList from './product-list'; 5 | 6 | export { ProductCard, ProductDetails, ProductFacets, ProductList }; 7 | -------------------------------------------------------------------------------- /src/components/designSystem/data-table/index.tsx: -------------------------------------------------------------------------------- 1 | import DataTable from './data-table'; 2 | import StatusSelect from './status-select'; 3 | import TableBody from './table-body'; 4 | import TableHead from './table-head'; 5 | import TableTitleBar from './table-title-bar'; 6 | 7 | export { dataTableDefinition } from './data-table'; 8 | export { DataTable, StatusSelect, TableBody, TableHead, TableTitleBar }; 9 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/content-error.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@/components/designSystem'; 2 | import { Alert } from '@material-tailwind/react'; 3 | 4 | export default function ContentError(error: any) { 5 | const { message, cause } = error; 6 | return ( 7 | 8 | 9 | {message} 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/layout/footer-slot/copyright.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSiteConfig } from '@/hooks'; 4 | import { Typography } from '@material-tailwind/react'; 5 | 6 | export default function Copyright() { 7 | const { siteConfig } = useSiteConfig(); 8 | 9 | return ( 10 | 11 | {siteConfig.copyright} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/local-storage-utils.tsx: -------------------------------------------------------------------------------- 1 | export const getItemFromLocalStorage = ( 2 | key: string, 3 | isJSON: boolean 4 | ): string => { 5 | return isJSON 6 | ? JSON.parse(localStorage.getItem(key)!) 7 | : localStorage.getItem(key)!; 8 | }; 9 | 10 | export const notItemInLocalStorage = (key: string): boolean => { 11 | return !localStorage.getItem(key); 12 | }; 13 | 14 | export const setItemInLocalStorage = (key: string, value: any): void => { 15 | localStorage.setItem(key, value); 16 | }; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .env 38 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/header-tools.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import MiniCart from '@/components/designSystem/carts/mini-cart'; 4 | import MiniProfile from '@/components/designSystem/users/mini-profile'; 5 | import { useAppContext } from '@/hooks'; 6 | 7 | export default function HeaderTools() { 8 | const { state } = useAppContext(); 9 | const userRoles: string[] = state.currentUserRoles; 10 | 11 | return ( 12 |
13 | 14 | {!userRoles.includes('administrator') && } 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/string-utils.tsx: -------------------------------------------------------------------------------- 1 | export const toTitleCase = (str: string): string | null => { 2 | if (!str) return null; 3 | return str.replace( 4 | /\w\S*/g, 5 | (text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase() 6 | ); 7 | }; 8 | 9 | export const formatMessage = ( 10 | message: string, 11 | ...values: string[] 12 | ): string | null => { 13 | if (!message) return null; 14 | let formattedMessage = message; 15 | values.forEach((value, idx) => { 16 | formattedMessage = formattedMessage.replace(`{${idx}}`, value); 17 | }); 18 | return formattedMessage; 19 | }; 20 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | output: 'standalone', 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: 'https', 9 | hostname: 10 | 'api.cm77gs48zv-contentfu1-d1-public.model-t.cc.commerce.ondemand.com', 11 | port: '', 12 | pathname: '/medias/**', 13 | }, 14 | { 15 | protocol: 'https', 16 | hostname: 'images.ctfassets.net', 17 | port: '', 18 | pathname: '/**', 19 | }, 20 | ], 21 | }, 22 | }; 23 | 24 | export default nextConfig; 25 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/edit-text.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@/components/designSystem'; 2 | import { Typography } from '@material-tailwind/react'; 3 | 4 | export default function EditText(props: any) { 5 | const { type } = props; 6 | return ( 7 |
8 | 9 | 10 | Click here to edit the {type} component 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | const withMT = require('@material-tailwind/react/utils/withMT'); 3 | 4 | const config: Config = withMT({ 5 | content: [ 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/utils/**/*.{js,ts,jsx,tsx,mdx}', 9 | ], 10 | theme: { 11 | extend: { 12 | container: { 13 | center: true, 14 | padding: '1rem', 15 | screens: { 16 | xl: '1224px', 17 | }, 18 | }, 19 | fontFamily: { 20 | sans: ['Inter', 'sans-serif'], 21 | }, 22 | }, 23 | }, 24 | plugins: [], 25 | }); 26 | export default config; 27 | -------------------------------------------------------------------------------- /src/mocks/currencies.tsx: -------------------------------------------------------------------------------- 1 | import { Currency } from '@/models/commerce-types'; 2 | 3 | const euro = { 4 | code: 'EUR', 5 | name: 'Euro', 6 | symbol: '€', 7 | }; 8 | 9 | const usdollar = { 10 | code: 'USD', 11 | name: 'US Dollar', 12 | symbol: '$', 13 | }; 14 | 15 | const MockCurrencies: Array = [euro, usdollar]; 16 | 17 | const getCurrencies = (): Array | null => { 18 | return MockCurrencies; 19 | }; 20 | 21 | const getCurrency = (code: string): Currency | null => { 22 | if (!code) return null; 23 | return MockCurrencies.find((currency) => currency.code === code) || null; 24 | }; 25 | 26 | export { MockCurrencies, euro, getCurrencies, getCurrency, usdollar }; 27 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/grid-button.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@/components/designSystem'; 2 | import { Button } from '@material-tailwind/react'; 3 | 4 | export default function GridButton(props: any) { 5 | const { variant, toggleLayout } = props; 6 | 7 | return ( 8 |
9 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Experience } from '@/components/experience'; 2 | import '@/components/studio-config'; 3 | import React from 'react'; 4 | 5 | type PageProps = { 6 | params: Promise<{ slug: string | string[] }>; 7 | searchParams: Promise<{ [key: string]: string | string[] | undefined }>; 8 | }; 9 | 10 | export default async function Page({ 11 | params, 12 | searchParams, 13 | }: PageProps): Promise { 14 | const { slug } = await params; 15 | const { expEditorMode } = await searchParams; 16 | 17 | return ( 18 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/mocks/languages.tsx: -------------------------------------------------------------------------------- 1 | import { Language } from '@/models/commerce-types'; 2 | 3 | const english = { 4 | isocode: 'en', 5 | name: 'English', 6 | nativeName: 'English', 7 | active: true, 8 | }; 9 | 10 | const german = { 11 | isocode: 'de', 12 | name: 'German', 13 | nativeName: 'Deutch', 14 | active: true, 15 | }; 16 | 17 | const MockLanguages: Array = [german, english]; 18 | 19 | const getLanguage = (isocode: string): Language | null => { 20 | if (!isocode) return null; 21 | return MockLanguages.find((lang) => lang.isocode === isocode) || null; 22 | }; 23 | 24 | const getLanguages = (): Array | null => { 25 | return MockLanguages; 26 | }; 27 | 28 | export { english, german, getLanguage, getLanguages, MockLanguages }; 29 | -------------------------------------------------------------------------------- /src/services/contentful/client.ts: -------------------------------------------------------------------------------- 1 | import { ContentfulClientApi, createClient } from 'contentful'; 2 | 3 | export const deliveryClient: ContentfulClientApi = createClient({ 4 | space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID || '', 5 | accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_DELIVERY_API_TOKEN || '', 6 | environment: process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT || 'master', 7 | }); 8 | 9 | export const previewClient: ContentfulClientApi = createClient({ 10 | space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID || '', 11 | accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_PREVIEW_API_TOKEN || '', 12 | environment: process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT || 'master', 13 | host: 'preview.contentful.com', 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/site-logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSiteConfig } from '@/hooks'; 4 | import { getContentfulImageUrl } from '@/utils/image-utils'; 5 | import Image from 'next/image'; 6 | import Link from 'next/link'; 7 | 8 | export default function SiteLogo() { 9 | const { siteConfig } = useSiteConfig(); 10 | const { siteLogo } = siteConfig; 11 | const source = 12 | (siteLogo && getContentfulImageUrl(siteLogo)) || 13 | '/images/powertoolz-logo-white.png'; 14 | 15 | return ( 16 | 17 | Site logo 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | import FooterSlot from './footer-slot'; 2 | import Copyright from './footer-slot/copyright'; 3 | import FooterNav from './footer-slot/footer-nav'; 4 | import SocialNav from './footer-slot/social-nav'; 5 | import HeaderSlot from './header-slot'; 6 | import EyebrowNavigation from './header-slot/eyebrow-navigation'; 7 | import HeaderTools from './header-slot/header-tools'; 8 | import LocaleSelector from './header-slot/locale-selector'; 9 | import PrimaryNav from './header-slot/primary-nav'; 10 | import SiteLogo from './header-slot/site-logo'; 11 | 12 | export { 13 | Copyright, 14 | EyebrowNavigation, 15 | FooterNav, 16 | FooterSlot, 17 | HeaderSlot, 18 | HeaderTools, 19 | LocaleSelector, 20 | PrimaryNav, 21 | SiteLogo, 22 | SocialNav, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CodeQL Scan for GitHub Actions Workflows" 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | paths: [".github/workflows/**"] 8 | pull_request: 9 | branches: [main] 10 | paths: [".github/workflows/**"] 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze GitHub Actions workflows 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v3 26 | with: 27 | languages: actions 28 | 29 | - name: Run CodeQL Analysis 30 | uses: github/codeql-action/analyze@v3 31 | with: 32 | category: actions 33 | -------------------------------------------------------------------------------- /src/components/layout/footer-slot/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useAppContext from '@/hooks/app-context'; 4 | import Copyright from './copyright'; 5 | import FooterNav from './footer-nav'; 6 | import SocialNav from './social-nav'; 7 | 8 | export default function FooterSlot() { 9 | const { state } = useAppContext(); 10 | const { currentUser } = state; 11 | 12 | return ( 13 | <> 14 | {currentUser !== '' && ( 15 |
16 | 17 |
18 | )} 19 |
20 | 21 | 22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/table-utils.tsx: -------------------------------------------------------------------------------- 1 | export const sortTableData = (tableData: any, sort: string) => { 2 | const [field, dir] = sort.split('-'); 3 | return dir === 'asc' 4 | ? sortTableDataAscending(tableData, field) 5 | : sortTableDataDescending(tableData, field); 6 | }; 7 | 8 | export const sortTableDataAscending = ( 9 | tableData: Array, 10 | field: string 11 | ) => { 12 | if (!tableData || !field) return null; 13 | return tableData.sort((a: any, b: any) => { 14 | if (a[field] > b[field]) return 1; 15 | if (a[field] < b[field]) return -1; 16 | return 0; 17 | }); 18 | }; 19 | 20 | export const sortTableDataDescending = ( 21 | tableData: Array, 22 | field: string 23 | ) => { 24 | if (!tableData || !field) return null; 25 | return tableData.sort((a: any, b: any) => { 26 | if (a[field] > b[field]) return -1; 27 | if (a[field] < b[field]) return 1; 28 | return 0; 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/mocks/cost_centers.tsx: -------------------------------------------------------------------------------- 1 | import { B2BCostCenter } from '@/models/commerce-types'; 2 | import { euro, usdollar } from './currencies'; 3 | 4 | export const bauhaus: B2BCostCenter = { 5 | code: '00005678', 6 | name: 'Kostenstelle 1', 7 | activeFlag: true, 8 | currency: euro, 9 | unit: undefined, 10 | assignedBudgets: [], 11 | }; 12 | 13 | export const diycompany: B2BCostCenter = { 14 | code: '00001234', 15 | name: 'Cost Center 1', 16 | activeFlag: true, 17 | currency: usdollar, 18 | unit: undefined, 19 | assignedBudgets: [], 20 | }; 21 | 22 | export const MockCostCenters: Array = [bauhaus, diycompany]; 23 | 24 | export const getCostCenter = (code: string): B2BCostCenter | null => { 25 | if (!code) return null; 26 | return MockCostCenters.find((cc) => cc.code === code) || null; 27 | }; 28 | 29 | export const getCostCenters = (): Array | null => { 30 | return MockCostCenters; 31 | }; 32 | -------------------------------------------------------------------------------- /src/hooks/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider } from '@material-tailwind/react'; 4 | import { AppProvider } from './app-context'; 5 | import { CartsProvider } from './carts-context'; 6 | import { EditModeProvider } from './edit-mode-context'; 7 | import { SiteConfigProvider } from './site-config-context'; 8 | import { SiteLabelsProvider } from './site-labels-context'; 9 | 10 | const Providers = ({ 11 | children, 12 | }: { 13 | children: React.ReactNode; 14 | }): JSX.Element => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | {children} 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Providers; 31 | -------------------------------------------------------------------------------- /src/hooks/site-labels-context.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type SiteLabelsContextType = { 4 | siteLabels: Record; 5 | setSiteLabels: (arg0: Record) => void; 6 | }; 7 | 8 | const SiteLabelsContext = React.createContext( 9 | null 10 | ); 11 | 12 | const SiteLabelsProvider = ({ children }: { children: React.ReactNode }) => { 13 | const [siteLabels, setSiteLabels] = React.useState>({}); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | const useSiteLabels = () => { 23 | const context = React.useContext(SiteLabelsContext); 24 | if (!context) { 25 | throw new Error('useLabels can only be used inside a LabelsProvider'); 26 | } 27 | return context; 28 | }; 29 | 30 | export default useSiteLabels; 31 | export { SiteLabelsContext, SiteLabelsProvider }; 32 | -------------------------------------------------------------------------------- /src/components/designSystem/products/product-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Product } from '@/models/commerce-types'; 4 | import ProductCard from './product-card'; 5 | 6 | export default function ProductList(props: any) { 7 | const { cols, products, variant, ...passedProps } = props; 8 | 9 | return ( 10 | <> 11 | {products?.map((product: Product, key: number) => { 12 | return ( 13 |
26 | 27 |
28 | ); 29 | })} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/mocks/addresses.tsx: -------------------------------------------------------------------------------- 1 | import { Address } from '@/models/commerce-types'; 2 | 3 | export const bauhaus: Address = { 4 | id: '0005678', 5 | street1: 'Hasenheide 109', 6 | city: 'Berlin', 7 | countryCode: 'DE', 8 | postalcode: '10967', 9 | defaultAddress: true, 10 | shippingAddress: true, 11 | visible: true, 12 | }; 13 | 14 | export const diycompany: Address = { 15 | id: '0001234', 16 | street1: '2709 Woodburn Ave', 17 | city: 'Cincinatti', 18 | stateOrProvince: 'OH', 19 | countryCode: 'US', 20 | postalcode: '45202', 21 | defaultAddress: true, 22 | shippingAddress: true, 23 | visible: true, 24 | }; 25 | 26 | export const MockAddresses: Array
= [bauhaus, diycompany]; 27 | 28 | export const getAddress = (id: string): Address | null => { 29 | if (!id) return null; 30 | return MockAddresses.find((address) => address.id === id) || null; 31 | }; 32 | 33 | export const getAddresses = (): Array
| null => { 34 | return MockAddresses; 35 | }; 36 | -------------------------------------------------------------------------------- /src/hooks/edit-mode-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | 4 | type EditModeContextType = { 5 | editMode: boolean; 6 | setEditMode: (mode: boolean) => void; 7 | }; 8 | 9 | const EditModeContext = React.createContext({ 10 | editMode: false, 11 | setEditMode: (mode) => null, 12 | }); 13 | 14 | const EditModeProvider = ({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }): React.JSX.Element => { 19 | const [editMode, setEditMode] = React.useState(false); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | const useEditMode = () => { 29 | const context = React.useContext(EditModeContext); 30 | if (!context) { 31 | throw new Error('useEditMode must be used inside the EditModeProvider'); 32 | } 33 | return context; 34 | }; 35 | 36 | export default useEditMode; 37 | export { EditModeContext, EditModeProvider }; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "react-jsx", 18 | "incremental": true, 19 | "exactOptionalPropertyTypes": false, 20 | "noImplicitReturns": true, 21 | "strictPropertyInitialization": false, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ], 27 | "paths": { 28 | "@/*": [ 29 | "./src/*" 30 | ] 31 | }, 32 | "target": "ES2017" 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts", 39 | ".next/dev/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useAppContext, { AppContext, AppProvider } from './app-context'; 2 | import useCartsContext, { 3 | CartAction, 4 | CartsContext, 5 | CartsProvider, 6 | CreateCartProps, 7 | UpdateCartEntriesProps, 8 | } from './carts-context'; 9 | import useEditMode, { 10 | EditModeContext, 11 | EditModeProvider, 12 | } from './edit-mode-context'; 13 | import Providers from './providers'; 14 | import useSiteConfig, { 15 | SiteConfigContext, 16 | SiteConfigProvider, 17 | } from './site-config-context'; 18 | import useSiteLabels, { 19 | SiteLabelsContext, 20 | SiteLabelsProvider, 21 | } from './site-labels-context'; 22 | 23 | export { 24 | AppContext, 25 | AppProvider, 26 | CartsContext, 27 | CartsProvider, 28 | EditModeContext, 29 | EditModeProvider, 30 | Providers, 31 | SiteConfigContext, 32 | SiteConfigProvider, 33 | SiteLabelsContext, 34 | SiteLabelsProvider, 35 | useAppContext, 36 | useCartsContext, 37 | useEditMode, 38 | useSiteConfig, 39 | useSiteLabels, 40 | }; 41 | export type { CartAction, CreateCartProps, UpdateCartEntriesProps }; 42 | -------------------------------------------------------------------------------- /src/hooks/site-config-context.tsx: -------------------------------------------------------------------------------- 1 | import { SiteConfigType } from '@/models/content-types'; 2 | import React from 'react'; 3 | 4 | type SiteConfigContextType = { 5 | siteConfig: SiteConfigType; 6 | setSiteConfig: (c: SiteConfigType) => void; 7 | }; 8 | 9 | const SiteConfigContext = React.createContext( 10 | null 11 | ); 12 | 13 | const SiteConfigProvider = ({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }): React.JSX.Element => { 18 | const [siteConfig, setSiteConfig] = React.useState({ 19 | locale: '', 20 | siteName: '', 21 | }); 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | const useSiteConfig = () => { 31 | const context = React.useContext(SiteConfigContext); 32 | if (!context) { 33 | throw new Error( 34 | 'useMSiteConfig must be used inside the SiteConfigProvider' 35 | ); 36 | } 37 | return context; 38 | }; 39 | 40 | export default useSiteConfig; 41 | export { SiteConfigContext, SiteConfigProvider }; 42 | -------------------------------------------------------------------------------- /src/components/slug-rewriter.tsx: -------------------------------------------------------------------------------- 1 | type RewriteRule = { 2 | pattern: RegExp; 3 | target: string; 4 | }; 5 | 6 | class SlugRewriter { 7 | public userRoles: string[]; 8 | public rules: Array; 9 | 10 | constructor(userRoles: string[]) { 11 | this.userRoles = userRoles; 12 | 13 | this.rules = [ 14 | { pattern: /^articles\/(.)+$/, target: 'articles/article-template' }, 15 | { 16 | pattern: /^dashboard$/, 17 | target: this.userRoles[0], 18 | }, 19 | { 20 | pattern: /^dashboard\/(.)*$/, 21 | target: this.userRoles[0], 22 | }, 23 | { pattern: /^products\/?$/, target: 'catalog' }, 24 | { pattern: /^products\/(.)+$/, target: 'products/product-template' }, 25 | ]; 26 | } 27 | 28 | public process(slug: string): string { 29 | if (!slug) return '/'; 30 | // if (/^\/?(.)*$/.test(slug) && !this.appState.currentUser) return '/'; 31 | 32 | let newSlug = slug; 33 | 34 | this.rules.forEach(({ pattern, target }) => { 35 | if (pattern.test(slug)) newSlug = target; 36 | }); 37 | 38 | return newSlug; 39 | } 40 | } 41 | 42 | export default SlugRewriter; 43 | -------------------------------------------------------------------------------- /src/services/sap/products.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import SAPClient from './sap-client'; 4 | 5 | const getProduct = async ({ code, locale }: any): Promise => { 6 | if (!code) 7 | throw new Error( 8 | 'Invalid number of arguments provided for the getProduct call' 9 | ); 10 | 11 | const client = new SAPClient(); 12 | const params = { fields: 'FULL', locale }; 13 | const endpoint = `products/${code}`; 14 | 15 | return await client 16 | .get(endpoint, params) 17 | .then((response) => { 18 | return response; 19 | }) 20 | .catch((error) => { 21 | throw error; 22 | }); 23 | }; 24 | 25 | const getProducts = async ( 26 | queryParams?: Record 27 | ): Promise => { 28 | const client = new SAPClient(); 29 | let params = { 30 | fields: 'FULL', 31 | }; 32 | if (queryParams) { 33 | params = { ...params, ...queryParams }; 34 | } 35 | 36 | return await client 37 | .get('products/search', params) 38 | .then((response) => { 39 | return response; 40 | }) 41 | .catch((error) => { 42 | throw error; 43 | }); 44 | }; 45 | 46 | export { getProduct, getProducts }; 47 | -------------------------------------------------------------------------------- /src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export { MockAddresses, getAddresses, getAddress } from './addresses'; 2 | export { MockB2BUnits, getB2BUnits, getB2BUnit } from './b2b_units'; 3 | export { MockCostCenters, getCostCenters, getCostCenter } from './cost_centers'; 4 | export { MockCountries, getCountries, getCountry } from './countries'; 5 | export { MockCurrencies, getCurrencies, getCurrency } from './currencies'; 6 | export { 7 | MockDeliveryModes, 8 | getDeliveryModes, 9 | getDeliveryMode, 10 | } from './delivery-modes'; 11 | export { MockLanguages, getLanguages, getLanguage } from './languages'; 12 | export { 13 | ORDER_DATA_COLS, 14 | MockOrders, 15 | getLastOrderForUser, 16 | getOrders, 17 | getOrdersByOrg, 18 | getOrdersByUser, 19 | getOrdersTableData, 20 | getOrder, 21 | } from './orders'; 22 | export { 23 | QUOTE_DATA_COLS, 24 | MockQuotes, 25 | getQuotes, 26 | getQuotesByOrg, 27 | getQuotesByUser, 28 | getQuotesTableData, 29 | getQuote, 30 | } from './quotes'; 31 | export { 32 | TICKET_DATA_COLS, 33 | MockTickets, 34 | getTickets, 35 | getTicketsByUser, 36 | getTicketsTableData, 37 | getTicket, 38 | } from './tickets'; 39 | export { MockUsers, getUser, getUsers } from './users'; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cx-ps-studio-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@contentful/experiences-sdk-react": "^1.34.1", 13 | "@contentful/rich-text-react-renderer": "^16.0.1", 14 | "@fortawesome/fontawesome-svg-core": "^6.7.1", 15 | "@fortawesome/free-brands-svg-icons": "^6.7.1", 16 | "@fortawesome/free-regular-svg-icons": "^6.7.1", 17 | "@fortawesome/free-solid-svg-icons": "^6.7.1", 18 | "@fortawesome/react-fontawesome": "^0.2.2", 19 | "@material-tailwind/react": "^2.1.10", 20 | "contentful": "^11.5.3", 21 | "next": "^16.0.7", 22 | "react": "^19.2.1", 23 | "react-dom": "^19.2.1", 24 | "react-multi-carousel": "^2.8.5" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^22.13.8", 28 | "@types/react": "^18.2.19", 29 | "@types/react-dom": "^18.3.5", 30 | "autoprefixer": "^10.4.19", 31 | "eslint": "^9.15.0", 32 | "eslint-config-next": "^15.2.0", 33 | "postcss": "^8.4.38", 34 | "tailwindcss": "^3.3.0" 35 | }, 36 | "overrides": { 37 | "react": "^19.2.1", 38 | "react-dom": "^19.2.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | BUNDLE_ANALYZE=false 2 | ENVIRONMENT_NAME=local 3 | 4 | # This URL is the domain your app is hosted on. It is used for example, for generating the url(s) needed for the SEO properties 5 | NEXT_PUBLIC_BASE_URL=http://localhost:3000/ 6 | 7 | #-------- CONTENTFUL ENVIRONMENT VARIABLES ---------# 8 | 9 | # Your current space ID: https://www.contentful.com/help/find-space-id/ 10 | NEXT_PUBLIC_CONTENTFUL_SPACE_ID= 11 | NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT= 12 | 13 | # Your current space Content Delivery API access token: https://www.contentful.com/developers/docs/references/content-delivery-api/ 14 | NEXT_PUBLIC_CONTENTFUL_DELIVERY_API_TOKEN= 15 | 16 | # Your current space Content Preview API access token: https://www.contentful.com/developers/docs/references/content-preview-api/ 17 | NEXT_PUBLIC_CONTENTFUL_PREVIEW_API_TOKEN= 18 | 19 | # The content ID for the site config document to be used by the application 20 | NEXT_PUBLIC_CONTENTFUL_SITE_CONFIG_ID= 21 | 22 | #-------- SAP ENVIRONMENT VARIABLES ---------# 23 | 24 | # The public API endpoint for your SAP Commerce Cloud environment. 25 | NEXT_PUBLIC_SAP_API_ENDPOINT=https://api.cm77gs48zv-contentfu1-d1-public.model-t.cc.commerce.ondemand.com 26 | 27 | # The ID of the SAP Commerce Cloud base site to be used. 28 | NEXT_PUBLIC_SAP_BASE_SITE=powertools-spa -------------------------------------------------------------------------------- /src/components/layout/footer-slot/social-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import Menu from '@/components/designSystem/navigation/menu'; 3 | import { useEditMode, useSiteConfig } from '@/hooks'; 4 | import { Typography } from '@material-tailwind/react'; 5 | import React from 'react'; 6 | 7 | export default function SocialNav(props: any) { 8 | const { editMode } = useEditMode(); 9 | const { siteConfig } = useSiteConfig(); 10 | 11 | const [menuItems, setMenuItems] = React.useState>(); 12 | 13 | React.useEffect(() => { 14 | let isMounted = true; 15 | 16 | if (isMounted) { 17 | const socialItems = siteConfig.socialMenu?.map((item) => { 18 | return item.fields; 19 | }); 20 | 21 | setMenuItems(socialItems); 22 | } 23 | 24 | return () => { 25 | isMounted = false; 26 | }; 27 | }, [siteConfig]); 28 | 29 | return ( 30 |
31 | {menuItems ? ( 32 | 33 | ) : ( 34 | editMode && ( 35 | 36 | social menu not loaded 37 | 38 | ) 39 | )} 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/designSystem/orders/order-details-status.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const OrderDetailsStatus = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 |
13 | 14 | {siteLabels[`label.${details.status}`]} 15 | 16 | 17 | {localizeDate(state.currentLocale, details.updateTime)} 18 | 19 |
20 | {details.status === 'shipped' && ( 21 | 26 | {siteLabels['label.trackDelivery']} 27 | 28 | )} 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/designSystem/quotes/quote-details-status.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const QuoteDetailsStatus = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 |
13 | 14 | {siteLabels[`label.${details.status}`]} 15 | 16 | 17 | {localizeDate(state.currentLocale, details.updateTime)} 18 | 19 |
20 | {details.status === 'shipped' && ( 21 | 26 | {siteLabels['label.trackDelivery']} 27 | 28 | )} 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/locale-utils.tsx: -------------------------------------------------------------------------------- 1 | export const getCurrencyIso = (locale: string): string | null => { 2 | if (!locale) return null; 3 | switch (locale) { 4 | case 'de-DE': 5 | return 'EUR'; 6 | case 'en-US': 7 | return 'USD'; 8 | default: 9 | throw new Error(`No curencyIso was found for the locale ${locale}`); 10 | } 11 | }; 12 | 13 | export const localizeCurrency = ( 14 | locale: string, 15 | value: number 16 | ): string | null => { 17 | if (!(locale && value)) return null; 18 | const currency = getCurrencyIso(locale) || undefined; 19 | return new Intl.NumberFormat(locale, { style: 'currency', currency }).format( 20 | value 21 | ); 22 | }; 23 | 24 | export const localizeDate = ( 25 | locale: string, 26 | date: string, 27 | includeTime?: boolean 28 | ): string | null => { 29 | if (!(locale && date)) return null; 30 | 31 | const datetime = new Date(date); 32 | if (!datetime) return null; 33 | 34 | let formatter = new Intl.DateTimeFormat(locale, { 35 | year: 'numeric', 36 | month: 'numeric', 37 | day: 'numeric', 38 | }); 39 | 40 | if (includeTime) { 41 | formatter = new Intl.DateTimeFormat(locale, { 42 | year: 'numeric', 43 | month: 'numeric', 44 | day: 'numeric', 45 | hour: '2-digit', 46 | minute: '2-digit', 47 | }); 48 | } 49 | return formatter.format(datetime); 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/image-utils.tsx: -------------------------------------------------------------------------------- 1 | import { Product, SAPImage } from '@/models/commerce-types'; 2 | import { Asset } from 'contentful'; 3 | 4 | const SAPEndpoint = process.env.NEXT_PUBLIC_SAP_API_ENDPOINT; 5 | 6 | const getContentfulImageDescription = ( 7 | image: Asset | string, 8 | fallback: string 9 | ): string => { 10 | if (typeof image === 'string') { 11 | if (!image.startsWith('https:')) { 12 | return fallback || ''; 13 | } 14 | } else { 15 | if (typeof image.fields.description === 'string') { 16 | return image.fields?.description || fallback || ''; 17 | } 18 | } 19 | return fallback || ''; 20 | }; 21 | 22 | const getContentfulImageUrl = (image: Asset | string): string | null => { 23 | if (!image) return null; 24 | if (typeof image === 'string') { 25 | if (!image.startsWith('https:')) { 26 | return 'https:' + image; 27 | } 28 | } else { 29 | return `https:${image.fields?.file?.url}`; 30 | } 31 | return null; 32 | }; 33 | 34 | const getSAPProductImageUrl = (product: Product): string | null => { 35 | const productImage: SAPImage | undefined = product?.images.find( 36 | (image: SAPImage) => image.format === 'product' 37 | ); 38 | if (!productImage) return null; 39 | const url = `${SAPEndpoint}${productImage.url}`; 40 | return url; 41 | }; 42 | 43 | export { 44 | getContentfulImageDescription, 45 | getContentfulImageUrl, 46 | getSAPProductImageUrl, 47 | }; 48 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { FooterSlot, HeaderSlot } from '@/components/layout'; 2 | import Providers from '@/hooks/providers'; 3 | import '@/styles/globals.css'; 4 | import { config } from '@fortawesome/fontawesome-svg-core'; 5 | import '@fortawesome/fontawesome-svg-core/styles.css'; 6 | import { Metadata } from 'next'; 7 | import { Inter } from 'next/font/google'; 8 | import React from 'react'; 9 | 10 | config.autoAddCss = false; 11 | 12 | const inter = Inter({ 13 | subsets: ['latin'], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Experiences | Contentful', 18 | description: 'Generated by Contentful Experiences.', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }): React.JSX.Element { 26 | return ( 27 | 28 | 29 | 32 |
33 | 34 |
35 |
36 | {children} 37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/mocks/b2b_units.tsx: -------------------------------------------------------------------------------- 1 | import { B2BUnit } from '@/models/commerce-types'; 2 | import { 3 | bauhaus as bauhausAddress, 4 | diycompany as diycompanyAddress, 5 | } from './addresses'; 6 | import { 7 | bauhaus as bauhausCostCenter, 8 | diycompany as diycompanyCostCenter, 9 | } from './cost_centers'; 10 | import { 11 | anna_schmidt, 12 | david_miller, 13 | emily_johnson, 14 | jonas_fischer, 15 | lukas_muller, 16 | sarah_anderson, 17 | } from './users'; 18 | 19 | const bauhaus = { 20 | uid: '0001234', 21 | name: 'BAUHAUS', 22 | active: true, 23 | addresses: [bauhausAddress], 24 | approvers: [anna_schmidt], 25 | managers: [anna_schmidt], 26 | administrators: [jonas_fischer], 27 | customers: [lukas_muller], 28 | costCenters: [bauhausCostCenter], 29 | }; 30 | 31 | const diycompany = { 32 | uid: '0005678', 33 | name: 'The DIY Company', 34 | active: true, 35 | addresses: [diycompanyAddress], 36 | approvers: [david_miller], 37 | managers: [david_miller], 38 | administrators: [sarah_anderson], 39 | customers: [emily_johnson], 40 | costCenters: [diycompanyCostCenter], 41 | }; 42 | 43 | const MockB2BUnits: Array = [bauhaus, diycompany]; 44 | 45 | const getB2BUnit = (uid: string): B2BUnit | null => { 46 | if (!uid) return null; 47 | return MockB2BUnits.find((b2b_units) => b2b_units.uid === uid) || null; 48 | }; 49 | 50 | const getB2BUnits = (): Array | null => { 51 | return MockB2BUnits; 52 | }; 53 | 54 | export { getB2BUnit, getB2BUnits, MockB2BUnits }; 55 | -------------------------------------------------------------------------------- /src/mocks/delivery-modes.tsx: -------------------------------------------------------------------------------- 1 | import { DeliveryMode } from '@/models/commerce-types'; 2 | 3 | export const fedex_standard: DeliveryMode = { 4 | code: 'fedex-standard', 5 | name: 'FedEx Standard', 6 | deliveryCost: { value: 99.99 }, 7 | }; 8 | 9 | export const fedex_priority: DeliveryMode = { 10 | code: 'fedex-priority', 11 | name: 'FedEx Priority', 12 | deliveryCost: { value: 149.99 }, 13 | }; 14 | 15 | export const dhl_standard: DeliveryMode = { 16 | code: 'dhl-standard', 17 | name: 'DHL Standard', 18 | deliveryCost: { value: 99.99 }, 19 | }; 20 | 21 | export const dhl_priority: DeliveryMode = { 22 | code: 'dhl-priority', 23 | name: 'DHL Priority', 24 | deliveryCost: { value: 149.99 }, 25 | }; 26 | 27 | export const ups_standard: DeliveryMode = { 28 | code: 'ups-standard', 29 | name: 'UPS Standard', 30 | deliveryCost: { value: 99.99 }, 31 | }; 32 | 33 | export const ups_priority: DeliveryMode = { 34 | code: 'ups-standard', 35 | name: 'UPS Standard', 36 | deliveryCost: { value: 149.99 }, 37 | }; 38 | 39 | export const MockDeliveryModes: Array = [ 40 | dhl_priority, 41 | dhl_standard, 42 | fedex_priority, 43 | fedex_standard, 44 | ups_priority, 45 | ups_standard, 46 | ]; 47 | 48 | export const getDeliveryMode = (code: string): DeliveryMode | null => { 49 | if (!code) return null; 50 | return MockDeliveryModes.find((mode) => mode.code === code) || null; 51 | }; 52 | 53 | export const getDeliveryModes = (): Array | null => { 54 | return MockDeliveryModes; 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/designSystem/orders/order-details-modal.tsx: -------------------------------------------------------------------------------- 1 | import { getOrder } from '@/mocks'; 2 | import React from 'react'; 3 | import { OrderDetailsEntries } from './order-details-entries'; 4 | import { OrderDetailsFooter } from './order-details-footer'; 5 | import { OrderDetailsHeader } from './order-details-header'; 6 | import { OrderDetailsStatus } from './order-details-status'; 7 | 8 | export default function OrderDetailsModal(props: any) { 9 | const { code } = props; 10 | 11 | const [details, setDetails] = React.useState(); 12 | const [error, setError] = React.useState(); 13 | 14 | React.useEffect(() => { 15 | let isMounted = true; 16 | 17 | const getOrderDetails = async () => { 18 | const orderDetails = await getOrder(code); 19 | if (!orderDetails) return; 20 | if (isMounted) { 21 | setDetails(orderDetails); 22 | } 23 | }; 24 | 25 | getOrderDetails(); 26 | return () => { 27 | isMounted = false; 28 | }; 29 | }, [code]); 30 | 31 | return ( 32 |
33 | {error &&
{error}
} 34 | 35 | {details && ( 36 | <> 37 |
38 | 39 | 40 |
41 | {details.entries && } 42 | 43 | 44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/designSystem/quotes/quote-details-modal.tsx: -------------------------------------------------------------------------------- 1 | import { getQuote } from '@/mocks'; 2 | import React from 'react'; 3 | import { QuoteDetailsEntries } from './quote-details-entries'; 4 | import { QuoteDetailsFooter } from './quote-details-footer'; 5 | import { QuoteDetailsHeader } from './quote-details-header'; 6 | import { QuoteDetailsStatus } from './quote-details-status'; 7 | 8 | export default function QuoteDetailsModal(props: any) { 9 | const { code } = props; 10 | 11 | const [details, setDetails] = React.useState(); 12 | const [error, setError] = React.useState(); 13 | 14 | React.useEffect(() => { 15 | let isMounted = true; 16 | 17 | const getQuoteDetails = async () => { 18 | const quoteDetails = await getQuote(code); 19 | if (!quoteDetails) return; 20 | if (isMounted) { 21 | setDetails(quoteDetails); 22 | } 23 | }; 24 | 25 | getQuoteDetails(); 26 | return () => { 27 | isMounted = false; 28 | }; 29 | }, [code]); 30 | 31 | return ( 32 |
33 | {error &&
{error}
} 34 | 35 | {details && ( 36 | <> 37 |
38 | 39 | 40 |
41 | {details.entries && } 42 | 43 | 44 | )} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/eyebrow-navigation.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Menu, QuickOrderModal } from '@/components/designSystem'; 4 | import { useSiteConfig } from '@/hooks'; 5 | import { useRouter } from 'next/navigation'; 6 | import React from 'react'; 7 | 8 | export default function EyebrowNavigation(props: any) { 9 | const { siteConfig } = useSiteConfig(); 10 | const router = useRouter(); 11 | 12 | const [menuItems, setMenuItems] = React.useState>(); 13 | const [showQuickOrder, setShowQuickOrder] = React.useState(false); 14 | 15 | React.useEffect(() => { 16 | let isMounted = true; 17 | 18 | const { eyebrowMenu } = siteConfig; 19 | const eyebrowItems = eyebrowMenu?.map((item: any) => { 20 | return item.fields; 21 | }); 22 | 23 | if (isMounted) { 24 | setMenuItems(eyebrowItems); 25 | } 26 | 27 | return () => { 28 | isMounted = false; 29 | }; 30 | }, [siteConfig]); 31 | 32 | const handleLinkClick = (url: string) => { 33 | if (url === 'quick-order') { 34 | toggleModal(); 35 | } else { 36 | router.push(url); 37 | } 38 | }; 39 | 40 | const toggleModal = () => { 41 | setShowQuickOrder(!showQuickOrder); 42 | }; 43 | 44 | return ( 45 | <> 46 |
47 | 52 |
53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/designSystem/content/heading.tsx: -------------------------------------------------------------------------------- 1 | import { HeadingFormats } from '@/components/designSystem/picker-options'; 2 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export default function Heading({ 6 | children, 7 | variant = 'h2', 8 | }: any): JSX.Element { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | 16 | export const headingDefinition: ComponentDefinition = { 17 | id: 'heading', 18 | name: 'Heading', 19 | category: 'Elements', 20 | thumbnailUrl: 21 | 'https://images.ctfassets.net/yv5x7043a54k/7avJgWeSnC7GY7uK28Sqcz/ed70e092856d9a5c92cbe28fe342752b/heading-solid.svg', 22 | tooltip: { 23 | description: 'This is a custom heading created for our design system.', 24 | }, 25 | builtInStyles: [ 26 | 'cfBackgroundColor', 27 | 'cfBorder', 28 | 'cfBorderRadius', 29 | 'cfFontSize', 30 | 'cfLetterSpacing', 31 | 'cfLineHeight', 32 | 'cfMargin', 33 | 'cfMaxWidth', 34 | 'cfPadding', 35 | 'cfTextAlign', 36 | 'cfTextColor', 37 | 'cfTextTransform', 38 | 'cfWidth', 39 | ], 40 | variables: { 41 | children: { 42 | displayName: 'Text', 43 | type: 'Text', 44 | defaultValue: 'Heading', 45 | }, 46 | variant: { 47 | displayName: 'Variant', 48 | type: 'Text', 49 | validations: { 50 | in: HeadingFormats, 51 | }, 52 | defaultValue: 'h2', 53 | group: 'style', 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/sorts.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSiteLabels } from '@/hooks'; 4 | import { Option, Select, Typography } from '@material-tailwind/react'; 5 | 6 | export default function Sorts(props: any) { 7 | const { siteLabels } = useSiteLabels(); 8 | const { handleChangeSort, sortOptions } = props; 9 | 10 | const getSelectedOption = () => { 11 | const selectedOption = sortOptions.find((option: any) => option.selected); 12 | return selectedOption.value; 13 | }; 14 | 15 | return ( 16 |
17 | {sortOptions && ( 18 | <> 19 | 23 | {siteLabels['label.sort']} 24 | 25 | 44 | 45 | )} 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/designSystem/data-table/table-head.tsx: -------------------------------------------------------------------------------- 1 | import { DataTableColumn } from '@/models/commerce-types'; 2 | import { PREVIEW_COLS } from './data-table'; 3 | import { 4 | TailwindBgColorsMap, 5 | TailwindTextColorsMap, 6 | } from '@/utils/tailwind-colors-utils'; 7 | 8 | export default function TableHead(props: any) { 9 | const { 10 | cellpadding = 'py-2', 11 | cols, 12 | data, 13 | headbg, 14 | headtext, 15 | editMode, 16 | siteLabels, 17 | } = props; 18 | 19 | const border = headbg !== 'black' ? 'border-b' : ''; 20 | const bgcolor = !['black', 'white', 'inherit'].includes(headbg) 21 | ? headbg + '-500' 22 | : headbg; 23 | const textcolor = !['black', 'white', 'inherit'].includes(headtext) 24 | ? headtext + '-500' 25 | : headtext; 26 | 27 | return ( 28 | 31 | 32 | {editMode && 33 | PREVIEW_COLS.map((col, key) => { 34 | return ( 35 | 36 | {col} 37 | 38 | ); 39 | })} 40 | {data && 41 | cols?.map((entry: DataTableColumn, key: number) => { 42 | return ( 43 | 47 | {siteLabels[`label.${entry.key}`]} 48 | 49 | ); 50 | })} 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/layout/footer-slot/footer-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Menu } from '@/components/designSystem'; 4 | import { useSiteConfig } from '@/hooks'; 5 | import { Typography } from '@material-tailwind/react'; 6 | import React from 'react'; 7 | 8 | export default function FooterNav() { 9 | const { siteConfig } = useSiteConfig(); 10 | 11 | const [menuItems, setMenuItems] = React.useState>(); 12 | 13 | React.useEffect(() => { 14 | let isMounted = true; 15 | 16 | if (isMounted) { 17 | const { footerMenu1, footerMenu2, footerMenu3 } = siteConfig; 18 | const menus = [footerMenu1, footerMenu2, footerMenu3]; 19 | 20 | const footerItems = menus?.map((menu) => { 21 | return menu?.map((item) => { 22 | return item.fields; 23 | }); 24 | }); 25 | 26 | setMenuItems(footerItems); 27 | } 28 | 29 | return () => { 30 | isMounted = false; 31 | }; 32 | }, [siteConfig]); 33 | 34 | return ( 35 |
36 | {menuItems && 37 | menuItems?.map((menu, key) => ( 38 |
39 | 43 | {menu?.[0].text} 44 | 45 | 50 |
51 | ))} 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/services/sap/sap-client.tsx: -------------------------------------------------------------------------------- 1 | const AIR_HEADER = 'xj823lbq'; 2 | 3 | class SAPClient { 4 | public readonly apiEndpoint = process.env.NEXT_PUBLIC_SAP_API_ENDPOINT; 5 | public readonly baseSite = process.env.NEXT_PUBLIC_SAP_BASE_SITE; 6 | 7 | constructor() {} 8 | 9 | public async get( 10 | endpoint: string, 11 | queryParams: string | Record = '' 12 | ): Promise { 13 | const url = this.buildURL(endpoint, queryParams); 14 | 15 | const response = await fetch(url, { 16 | method: 'GET', 17 | headers: this.buildRequestHeaders(), 18 | }); 19 | 20 | await this.handleApiError(response); 21 | 22 | const data: any = await response.json(); 23 | return data; 24 | } 25 | 26 | private buildRequestHeaders() { 27 | return { 28 | 'content-type': 'application/json', 29 | 'application-interface-key': AIR_HEADER, 30 | }; 31 | } 32 | 33 | private buildURL( 34 | endpoint: string, 35 | queryParams?: string | Record 36 | ): string { 37 | const params = queryParams ? new URLSearchParams(queryParams) : undefined; 38 | const url = `${this.apiEndpoint}/occ/v2/${this.baseSite}/${endpoint}/${ 39 | params ? '?' + params.toString() : '' 40 | }`; 41 | return url; 42 | } 43 | 44 | // basic error handling 45 | private async handleApiError(response: Response): Promise { 46 | if (response.status < 400) return; 47 | const errorResponse: string = await response.text(); 48 | const { statusText } = response; 49 | const msg = `SAP API error: ${errorResponse} [status: ${statusText}]`; 50 | throw new Error(msg); 51 | } 52 | } 53 | 54 | export default SAPClient; 55 | -------------------------------------------------------------------------------- /src/components/designSystem/content/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 2 | 3 | export const avatarDefinition: ComponentDefinition = { 4 | id: 'avatar', 5 | name: 'Avatar', 6 | category: 'Elements', 7 | thumbnailUrl: 8 | 'https://images.ctfassets.net/yv5x7043a54k/76GucUSCMZvTlrJW44jJvf/1febf72a3ecda078a7423b55213cff6b/circle-user-regular.svg', 9 | tooltip: { 10 | description: `Displays a user avatar.`, 11 | }, 12 | builtInStyles: [ 13 | 'cfBackgroundColor', 14 | 'cfBorder', 15 | 'cfBorderRadius', 16 | 'cfMargin', 17 | 'cfMaxWidth', 18 | 'cfPadding', 19 | 'cfWidth', 20 | ], 21 | variables: { 22 | src: { 23 | displayName: 'Source', 24 | type: 'Media', 25 | }, 26 | variant: { 27 | displayName: 'Variant', 28 | type: 'Text', 29 | validations: { 30 | in: [ 31 | { value: '', displayName: 'Circular' }, 32 | { value: 'rounded', displayName: 'Rounded' }, 33 | { value: 'square', displayName: 'Square' }, 34 | ], 35 | }, 36 | group: 'style', 37 | }, 38 | size: { 39 | displayName: 'Size', 40 | type: 'Text', 41 | validations: { 42 | in: [ 43 | { value: 'xs', displayName: 'XS' }, 44 | { value: 'sm', displayName: 'SM' }, 45 | { value: 'md', displayName: 'MD' }, 46 | { value: 'lg', displayName: 'LG' }, 47 | { value: 'xl', displayName: 'XL' }, 48 | { value: 'xxl', displayName: 'XXL' }, 49 | ], 50 | }, 51 | defaultValue: 'lg', 52 | group: 'style', 53 | }, 54 | withBorder: { 55 | displayName: 'Border', 56 | type: 'Boolean', 57 | defaultValue: false, 58 | group: 'style', 59 | }, 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/designSystem/tickets/ticket-details-header.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const TicketDetailsHeader = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 |
13 | {siteLabels['label.ticketNumber']} 14 | 15 | {details.id} 16 | 17 |
18 |
19 | {siteLabels['label.creationTime']} 20 | 21 | {localizeDate(state.currentLocale, details.createdAt)} 22 | 23 |
24 |
25 | {siteLabels['label.updateTime']} 26 | 27 | {localizeDate(state.currentLocale, details.modifiedAt)} 28 | 29 |
30 |
31 | {siteLabels['label.status']} 32 | 33 | {details.status} 34 | 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/designSystem/search/search-box.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon } from '@/components/designSystem'; 4 | import { useSiteLabels } from '@/hooks'; 5 | import { Button, Input } from '@material-tailwind/react'; 6 | import { useRouter } from 'next/navigation'; 7 | import React from 'react'; 8 | 9 | export default function SearchBox() { 10 | const { siteLabels } = useSiteLabels(); 11 | const router = useRouter(); 12 | 13 | const [formData, setFormData] = React.useState | null>(); 17 | 18 | const handleQueryChange = (e: React.ChangeEvent): void => { 19 | const value = e.currentTarget.value; 20 | if (!value) return; 21 | setFormData({ q: value }); 22 | }; 23 | 24 | const handleSubmit = (): void => { 25 | if (!formData) return; 26 | const paranms = new URLSearchParams(formData); 27 | const searchUrl = `/search?${paranms.toString()}`; 28 | router.push(searchUrl); 29 | }; 30 | 31 | const handleKeyDown = (event: React.KeyboardEvent) => { 32 | const { code, key } = event; 33 | if (code.toLowerCase() === 'enter' && key.toLowerCase() === 'enter') { 34 | handleSubmit(); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | handleQueryChange(e)} 46 | onKeyDown={(e) => handleKeyDown(e)} 47 | type='text' 48 | /> 49 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/content-utils.tsx: -------------------------------------------------------------------------------- 1 | import { Asset, BaseEntry, Entry } from 'contentful'; 2 | 3 | export const getAssetUrl = (asset: Asset | null): string | null => { 4 | if (!asset) return null; 5 | return asset?.fields.file?.url as string; 6 | }; 7 | 8 | export const getContentType = (entry: BaseEntry): string => { 9 | return entry.sys.contentType.sys.id; 10 | }; 11 | 12 | export const getLinkedAsset = ( 13 | id: string, 14 | assets: Array 15 | ): Asset | null => { 16 | if (!id) return null; 17 | return assets.find((asset) => asset.sys.id === id) || null; 18 | }; 19 | 20 | export const getLinkedEntry = ( 21 | id: string, 22 | entries: Array 23 | ): Entry | null => { 24 | return entries.find((entry) => entry.sys.id === id) || null; 25 | }; 26 | 27 | export const getSocialChannelName = (url: string | null): string | null => { 28 | if (!url) return null; 29 | const capturingRegex = /(?:facebook|twitter|instagram|linkedin|youtube)/; 30 | return '' + url.match(capturingRegex); 31 | }; 32 | 33 | export const getSocialIcon = (channel: string | null): [string, any] | [] => { 34 | if (!channel) []; 35 | let color, icon; 36 | 37 | switch (channel) { 38 | case 'facebook': 39 | color = 'royalblue'; 40 | icon = 'facebook'; 41 | break; 42 | case 'instagram': 43 | color = 'darkviolet'; 44 | icon = 'instagram'; 45 | break; 46 | case 'linkedin': 47 | color = 'darkblue'; 48 | icon = 'linkedin'; 49 | break; 50 | case 'twitter': 51 | color = 'deepskyblue'; 52 | icon = 'twitter'; 53 | break; 54 | case 'x': 55 | color = 'black'; 56 | icon = 'x-twitter'; 57 | break; 58 | case 'youtube': 59 | color = 'firebrick'; 60 | icon = 'youtube'; 61 | break; 62 | default: 63 | color = 'slategray'; 64 | icon = 'arrow-up-right-from-square'; 65 | } 66 | 67 | return [color, icon]; 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/designSystem/data-table/table-title-bar.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@material-tailwind/react'; 2 | import StatusSelect from './status-select'; 3 | import { 4 | TailwindBgColorsMap, 5 | TailwindTextColorsMap, 6 | } from '@/utils/tailwind-colors-utils'; 7 | 8 | export default function TableTitleBar(props: any) { 9 | const { 10 | datatype, 11 | filter, 12 | handleOptionClick, 13 | editMode, 14 | setSortOpen, 15 | siteLabels, 16 | sortOpen, 17 | sortOptions, 18 | titlebg, 19 | titletext, 20 | } = props; 21 | 22 | const bgcolor = !['black', 'white', 'inherit'].includes(titlebg) 23 | ? titlebg + '-500' 24 | : titlebg; 25 | 26 | const textcolor = !['black', 'white', 'inherit'].includes(titletext) 27 | ? titletext + '-500' 28 | : titletext; 29 | 30 | return ( 31 | <> 32 | {editMode ? ( 33 |
36 | {datatype} data table - data is dynamically loaded based on the signed 37 | in user 38 |
39 | ) : ( 40 |
43 | 48 | {siteLabels[`label.${datatype}`]} 49 | 50 | 60 |
61 | )} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/hooks/app-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | type AppContextType = { 6 | state: AppContextState; 7 | updateState: (arg: AppContextState) => void; 8 | logout: () => void; 9 | }; 10 | 11 | export type AppContextState = { 12 | currentLocale: string; 13 | currentOrgUnit: string; 14 | currentUser: string; 15 | currentUserRoles: Array; 16 | }; 17 | 18 | const AppContext = React.createContext({ 19 | state: { 20 | currentLocale: '', 21 | currentOrgUnit: '', 22 | currentUser: '', 23 | currentUserRoles: new Array(), 24 | }, 25 | updateState: (state: AppContextState) => null, 26 | logout: () => null, 27 | }); 28 | 29 | const defaultState = { 30 | currentLocale: '', 31 | currentOrgUnit: '', 32 | currentUser: '', 33 | currentUserRoles: new Array(), 34 | }; 35 | 36 | const AppProvider = ({ 37 | children, 38 | }: { 39 | children: React.ReactNode; 40 | }): React.JSX.Element => { 41 | const [state, setState] = React.useState(defaultState); 42 | 43 | React.useEffect(() => { 44 | const savedState = sessionStorage.getItem('cf-b2b-state'); 45 | if (savedState) { 46 | setState(JSON.parse(savedState)); 47 | } 48 | }, []); 49 | 50 | const logout = () => { 51 | setState(defaultState); 52 | sessionStorage.removeItem('cf-b2b-state'); 53 | }; 54 | 55 | const updateState = (newState: AppContextState) => { 56 | setState(newState); 57 | sessionStorage.setItem('cf-b2b-state', JSON.stringify(newState)); 58 | }; 59 | 60 | return ( 61 | 68 | {children} 69 | 70 | ); 71 | }; 72 | 73 | const useAppContext = (): AppContextType => { 74 | return React.useContext(AppContext); 75 | }; 76 | 77 | export default useAppContext; 78 | export { AppContext, AppProvider }; 79 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body {} 20 | 21 | @layer base { 22 | h1 { 23 | @apply text-4xl font-semibold; 24 | @apply mb-3 mt-0; 25 | line-height: 1; 26 | } 27 | 28 | h2 { 29 | @apply text-2xl font-semibold; 30 | @apply mb-3 mt-0; 31 | } 32 | 33 | h3 { 34 | @apply text-xl font-semibold; 35 | @apply mb-3 mt-0; 36 | } 37 | 38 | h4 { 39 | @apply text-lg font-semibold; 40 | } 41 | 42 | p { 43 | @apply mb-3 mt-0; 44 | } 45 | 46 | /* div[data-ctfl-draggable-id]:has(> div.w-full) { 47 | width: 100%; 48 | } */ 49 | 50 | div[data-component-wrapper]:has(> div.w-full) { 51 | width: 100%; 52 | } 53 | 54 | 55 | /* ul { 56 | @apply list-disc; 57 | @apply ml-6 my-4 pl-10; 58 | line-height: 1.25; 59 | } */ 60 | 61 | .border, 62 | .border-t, 63 | .border-b, 64 | .border-l, 65 | .border-r { 66 | @apply border-gray-800; 67 | } 68 | 69 | .mock-data p { 70 | @apply m-0 text-inherit; 71 | } 72 | 73 | .mock-data table { 74 | @apply table-auto; 75 | width: 100%; 76 | } 77 | 78 | .mock-data td, 79 | .mock-data th { 80 | @apply text-center; 81 | @apply px-2; 82 | } 83 | 84 | .mock-data td:first-child, 85 | .mock-data th:first-child { 86 | @apply text-start; 87 | } 88 | 89 | .mock-data td:last-child, 90 | .mock-data th:last-child { 91 | @apply text-end; 92 | } 93 | 94 | } 95 | 96 | @layer utilities { 97 | .text-balance { 98 | text-wrap: balance; 99 | } 100 | } -------------------------------------------------------------------------------- /src/components/designSystem/content/alert.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Icons, 3 | TailwindColors, 4 | } from '@/components/designSystem/picker-options'; 5 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 6 | 7 | export const alertDefinition: ComponentDefinition = { 8 | id: 'alert', 9 | name: 'Alert', 10 | category: 'Elements', 11 | thumbnailUrl: 12 | 'https://images.ctfassets.net/yv5x7043a54k/3f1YxuUm4hSY0DdExuhJMN/936522a8865031e17165ead707e430ae/circle-exclamation-solid.svg', 13 | tooltip: { 14 | description: `Displays a short and important message attracting the user's attention without interrupting the user's task.`, 15 | }, 16 | builtInStyles: [ 17 | 'cfBackgroundColor', 18 | 'cfBorder', 19 | 'cfBorderRadius', 20 | 'cfFontSize', 21 | 'cfLetterSpacing', 22 | 'cfLineHeight', 23 | 'cfMargin', 24 | 'cfMaxWidth', 25 | 'cfPadding', 26 | 'cfTextAlign', 27 | 'cfTextColor', 28 | 'cfTextTransform', 29 | 'cfWidth', 30 | ], 31 | variables: { 32 | children: { 33 | displayName: 'Text', 34 | type: 'Text', 35 | defaultValue: 'Add your text here ...', 36 | }, 37 | variant: { 38 | displayName: 'Variant', 39 | type: 'Text', 40 | validations: { 41 | in: [ 42 | { value: '', displayName: 'Basic' }, 43 | { value: 'gradient', displayName: 'Gradient' }, 44 | { value: 'outlined', displayName: 'Outlined' }, 45 | { value: 'ghost', displayName: 'Ghost' }, 46 | ], 47 | }, 48 | group: 'style', 49 | }, 50 | color: { 51 | displayName: 'Color', 52 | type: 'Text', 53 | validations: { 54 | in: TailwindColors, 55 | }, 56 | defaultValue: 'amber', 57 | group: 'style', 58 | }, 59 | className: { 60 | displayName: 'Classes', 61 | type: 'Text', 62 | group: 'style', 63 | }, 64 | icon: { 65 | displayName: 'Icon', 66 | type: 'Object', 67 | validations: { 68 | in: Icons, 69 | }, 70 | group: 'style', 71 | }, 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /src/mocks/countries.tsx: -------------------------------------------------------------------------------- 1 | import { Country } from '@/models/commerce-types'; 2 | import { english, german } from './languages'; 3 | 4 | const germany: Country = { 5 | name: 'Germany', 6 | capital: 'Berlin', 7 | code: 'DE', 8 | currencies: [ 9 | { 10 | code: 'EUR', 11 | name: 'Euro', 12 | symbol: '€', 13 | }, 14 | ], 15 | languages: [german], 16 | coordinates: [51, 9], 17 | area: 357114, 18 | maps: { 19 | googleMaps: 'https://goo.gl/maps/mD9FBMq1nvXUBrkv6', 20 | openStreetMaps: 'https://www.openstreetmap.org/relation/51477', 21 | }, 22 | population: 83240525, 23 | postalCode: { 24 | format: '#####', 25 | regex: '^(\\d{5})$', 26 | }, 27 | flags: { 28 | png: 'https://flagcdn.com/w320/de.png', 29 | svg: 'https://flagcdn.com/de.svg', 30 | }, 31 | emoji: '🇩🇪', 32 | countryCallingCode: '+49', 33 | }; 34 | 35 | const us: Country = { 36 | name: 'United States', 37 | capital: 'Washington, D.C.', 38 | code: 'US', 39 | currencies: [ 40 | { 41 | code: 'USD', 42 | name: 'United States dollar', 43 | symbol: '$', 44 | }, 45 | ], 46 | languages: [english], 47 | coordinates: [38, -97], 48 | area: 9372610, 49 | maps: { 50 | googleMaps: 'https://goo.gl/maps/e8M246zY4BSjkjAv6', 51 | openStreetMaps: 52 | 'https://www.openstreetmap.org/relation/148838#map=2/20.6/-85.8', 53 | }, 54 | population: 329484123, 55 | postalCode: { 56 | format: '#####-####', 57 | regex: '^\\d{5}(-\\d{4})?$', 58 | }, 59 | flags: { 60 | png: 'https://flagcdn.com/w320/us.png', 61 | svg: 'https://flagcdn.com/us.svg', 62 | }, 63 | emoji: '🇺🇸', 64 | countryCallingCode: '+1', 65 | }; 66 | 67 | const MockCountries: Array = [germany, us]; 68 | 69 | const getCountries = (): Array | null => { 70 | return MockCountries; 71 | }; 72 | 73 | const getCountry = (code: string): Country | null => { 74 | if (!code) return null; 75 | return MockCountries.find((country) => country.code === code) || null; 76 | }; 77 | 78 | export { MockCountries, germany, getCountries, getCountry, us }; 79 | -------------------------------------------------------------------------------- /src/components/designSystem/tickets/ticket-details-modal.tsx: -------------------------------------------------------------------------------- 1 | import { useSiteLabels } from '@/hooks'; 2 | import { getTicket } from '@/mocks'; 3 | import { Typography } from '@material-tailwind/react'; 4 | import React from 'react'; 5 | import { TicketDetailsEvents } from './ticket-details-events'; 6 | import { TicketDetailsHeader } from './ticket-details-header'; 7 | 8 | export default function OrderDetailsModal(props: any) { 9 | const { code } = props; 10 | const { siteLabels } = useSiteLabels(); 11 | 12 | const [details, setDetails] = React.useState(); 13 | const [error, setError] = React.useState(); 14 | 15 | React.useEffect(() => { 16 | let isMounted = true; 17 | 18 | const getTicketDetails = async () => { 19 | const ticketDetails = await getTicket(code); 20 | if (!ticketDetails) return; 21 | if (isMounted) { 22 | setDetails(ticketDetails); 23 | } 24 | }; 25 | 26 | getTicketDetails(); 27 | return () => { 28 | isMounted = false; 29 | }; 30 | }, [code]); 31 | 32 | return ( 33 |
34 | {error &&
{error}
} 35 | 36 | {details && ( 37 | <> 38 |
39 |
40 | 45 | {siteLabels['label.subject']}: 46 | 47 | 52 | {details.subject} 53 | 54 |
55 | 56 | {details.ticketEvents && } 57 |
58 | 59 | )} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/designSystem/data-table/status-select.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | List, 4 | ListItem, 5 | Popover, 6 | PopoverContent, 7 | PopoverHandler, 8 | Typography, 9 | } from '@material-tailwind/react'; 10 | import Icon from '../content/icon'; 11 | 12 | export default function StatusSelect(props: any) { 13 | const { 14 | filter, 15 | handleOptionClick, 16 | setSortOpen, 17 | siteLabels, 18 | sortOpen, 19 | sortOptions, 20 | } = props; 21 | 22 | return ( 23 |
24 | 25 | 26 | 45 | 46 | 47 | 48 | {sortOptions?.map((opt: any, key: number) => { 49 | return ( 50 | handleOptionClick(opt?.toLowerCase())} 54 | ripple={false} 55 | > 56 | {siteLabels['label.' + opt]?.toLowerCase()} 57 | 58 | ); 59 | })} 60 | 61 | 62 | 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/designSystem/users/logout-modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useSiteLabels } from '@/hooks'; 3 | import { formatMessage } from '@/utils/string-utils'; 4 | import { 5 | Dialog, 6 | DialogBody, 7 | DialogHeader, 8 | Typography, 9 | } from '@material-tailwind/react'; 10 | import { useRouter } from 'next/navigation'; 11 | import React from 'react'; 12 | 13 | export default function LogoutModal(props: any) { 14 | const { showLogoutModal, setShowLogoutModal } = props; 15 | const { siteLabels } = useSiteLabels(); 16 | const router = useRouter(); 17 | 18 | const [secondsRemaining, setSecondsRemaining] = React.useState(5); 19 | 20 | let intervalId = React.useRef(null); 21 | 22 | const reduceSecondsRemaining = React.useCallback(() => { 23 | setSecondsRemaining((sr) => sr - 1); 24 | }, []); 25 | 26 | React.useEffect(() => { 27 | if (showLogoutModal) { 28 | if (!intervalId.current) { 29 | intervalId.current = setInterval(reduceSecondsRemaining, 1000); 30 | } 31 | 32 | if (secondsRemaining < 1) { 33 | clearInterval(intervalId.current); 34 | intervalId.current = null; 35 | setShowLogoutModal(false); 36 | router.push('/'); 37 | } 38 | } 39 | 40 | return () => { 41 | if (intervalId.current) { 42 | clearInterval(intervalId.current); 43 | intervalId.current = null; 44 | } 45 | }; 46 | }, [ 47 | showLogoutModal, 48 | setShowLogoutModal, 49 | secondsRemaining, 50 | reduceSecondsRemaining, 51 | router, 52 | ]); 53 | 54 | const toggleShowLogoutModal = () => { 55 | setShowLogoutModal((state: boolean) => !state); 56 | }; 57 | 58 | return ( 59 | 60 | {siteLabels['label.logoutSuccessful']} 61 | 62 | 63 | {formatMessage( 64 | siteLabels['message.logoutRedirect'], 65 | '' + secondsRemaining 66 | )} 67 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/designSystem/content/rating.tsx: -------------------------------------------------------------------------------- 1 | import { TailwindColors } from '@/components/designSystem/picker-options'; 2 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 3 | import Icon from './icon'; 4 | 5 | const stars = [1, 2, 3, 4, 5]; 6 | 7 | export default function Rating(props: any) { 8 | const { ratedColor, value = 0 } = props; 9 | return ( 10 |
11 | {stars.map((i: number) => { 12 | return ( 13 | 19 | ); 20 | })} 21 |
22 | ); 23 | } 24 | 25 | export const ratingDefinition: ComponentDefinition = { 26 | id: 'rating', 27 | name: 'Rating', 28 | category: 'Elements', 29 | thumbnailUrl: 30 | 'https://images.ctfassets.net/yv5x7043a54k/1WOYb0DAM5s5yFQXg9EO8i/f76d176f90d7a09c519c30bcf21a8758/star-half-stroke-solid.svg', 31 | tooltip: { 32 | description: 'A star rating with a scale of 0 to 5.', 33 | }, 34 | builtInStyles: [ 35 | 'cfBackgroundColor', 36 | 'cfBorder', 37 | 'cfBorderRadius', 38 | 'cfFontSize', 39 | 'cfMargin', 40 | 'cfMaxWidth', 41 | 'cfPadding', 42 | 'cfTextColor', 43 | 'cfWidth', 44 | ], 45 | variables: { 46 | value: { 47 | displayName: 'Rating', 48 | type: 'Number', 49 | defaultValue: 0, 50 | validations: { 51 | in: [ 52 | { displayName: '0', value: 0 }, 53 | { displayName: '1', value: 1 }, 54 | { displayName: '2', value: 2 }, 55 | { displayName: '3', value: 3 }, 56 | { displayName: '4', value: 4 }, 57 | { displayName: '5', value: 5 }, 58 | ], 59 | }, 60 | }, 61 | ratedColor: { 62 | displayName: 'Rated Color', 63 | type: 'Text', 64 | validations: { 65 | in: TailwindColors, 66 | }, 67 | defaultValue: 'amber', 68 | group: 'style', 69 | }, 70 | readOnly: { 71 | displayName: 'Readonly', 72 | type: 'Boolean', 73 | group: 'style', 74 | defaultValue: true, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/designSystem/navigation/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useEditMode, useSiteLabels } from '@/hooks'; 3 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 4 | import { Breadcrumbs as Crumbs } from '@material-tailwind/react'; 5 | import Link from 'next/link'; 6 | import { usePathname } from 'next/navigation'; 7 | 8 | const previewCrumbs = ['folder 1', 'folder 2', 'page']; 9 | 10 | export default function Breadcrumbs(props: any) { 11 | const { editMode } = useEditMode(); 12 | const { siteLabels } = useSiteLabels(); 13 | const pathname = usePathname(); 14 | const crumbs = pathname.substring(1).split('/'); 15 | 16 | const buildUrlForCrumb = (idx: number) => { 17 | let url = ''; 18 | crumbs.forEach((crumb, key) => { 19 | if (key <= idx) { 20 | url = url + '/' + crumbs[key]; 21 | } 22 | }); 23 | return url; 24 | }; 25 | 26 | return ( 27 | <> 28 | 29 | {siteLabels['label.dashboard']} 30 | {editMode && 31 | previewCrumbs.map((crumb: string, key: number) => { 32 | return ( 33 | 34 | {crumb} 35 | 36 | ); 37 | })} 38 | {crumbs.map((crumb: string, key: number) => { 39 | const url = buildUrlForCrumb(key); 40 | return ( 41 | 42 | {siteLabels[crumb] || decodeURIComponent(crumb)} 43 | 44 | ); 45 | })} 46 | 47 | 48 | ); 49 | } 50 | 51 | export const breadcrumbsDefinition: ComponentDefinition = { 52 | id: 'breadcrumb', 53 | name: 'Breadcrumb', 54 | category: 'Elements', 55 | thumbnailUrl: 56 | 'https://images.ctfassets.net/yv5x7043a54k/693vKsW5QrtTKnMsmyNl1R/2def409181f92b5ed64037ac00867f90/ellipsis-solid.svg?h=32&w=32', 57 | tooltip: { 58 | description: 'renders a breadcrumb for navigation', 59 | }, 60 | builtInStyles: [ 61 | 'cfBackgroundColor', 62 | 'cfBorder', 63 | 'cfBorderRadius', 64 | 'cfFontSize', 65 | 'cfLetterSpacing', 66 | 'cfLineHeight', 67 | 'cfMargin', 68 | 'cfMaxWidth', 69 | 'cfPadding', 70 | 'cfTextAlign', 71 | 'cfTextColor', 72 | 'cfTextTransform', 73 | 'cfWidth', 74 | ], 75 | variables: {}, 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/designSystem/content/products/product-carousel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { ProductCard } from '@/components/designSystem'; 3 | import { useEditMode } from '@/hooks'; 4 | import { Product } from '@/models/commerce-types'; 5 | import { Typography } from '@material-tailwind/react'; 6 | import Carousel from 'react-multi-carousel'; 7 | import 'react-multi-carousel/lib/styles.css'; 8 | 9 | export default function ProductCarousel(props: any) { 10 | const { editMode } = useEditMode(); 11 | const { products, ...passedProps } = props; 12 | 13 | const responsive = { 14 | xxl: { 15 | breakpoint: { max: 3400, min: 1141 }, 16 | items: Number(props.xxlcols) || 6, 17 | }, 18 | xl: { 19 | breakpoint: { max: 1140, min: 961 }, 20 | items: Number(props.xlcols) || 5, 21 | }, 22 | lg: { 23 | breakpoint: { max: 960, min: 721 }, 24 | items: Number(props.lgcols) || 4, 25 | }, 26 | md: { 27 | breakpoint: { max: 720, min: 541 }, 28 | items: 2, 29 | }, 30 | sm: { 31 | breakpoint: { max: 540, min: 0 }, 32 | items: 1, 33 | }, 34 | }; 35 | 36 | return ( 37 | <> 38 | {editMode && ( 39 | 40 |   41 | 42 | )} 43 | 52 | {products.map((product: Product, key: number) => { 53 | return ( 54 |
64 | 72 |
73 | ); 74 | })} 75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/designSystem/content/button.tsx: -------------------------------------------------------------------------------- 1 | import { TailwindColors } from '@/components/designSystem/picker-options'; 2 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 3 | import { Button as MTButton } from '@material-tailwind/react'; 4 | import Link from 'next/link'; 5 | 6 | export default function Button(props: any) { 7 | const { url, value, ...buttonProps } = props; 8 | 9 | return ( 10 | 11 | 12 | {value} 13 | 14 | 15 | ); 16 | } 17 | 18 | export const buttonDefinition: ComponentDefinition = { 19 | id: 'button', 20 | name: 'Button', 21 | category: 'Elements', 22 | thumbnailUrl: 23 | 'https://images.ctfassets.net/yv5x7043a54k/CZAJK1QcIjZMZ0e4GV5gC/59db38777e8bbdf8014ce38662cb11b0/circle-play-solid.svg', 24 | // thumbnailUrl: 25 | // 'https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600', 26 | tooltip: { 27 | description: 'This is a custom button created for our design system.', 28 | }, 29 | builtInStyles: [ 30 | 'cfFontSize', 31 | 'cfLetterSpacing', 32 | 'cfLineHeight', 33 | 'cfMargin', 34 | 'cfMaxWidth', 35 | 'cfPadding', 36 | 'cfTextAlign', 37 | 'cfTextColor', 38 | 'cfTextTransform', 39 | 'cfWidth', 40 | ], 41 | variables: { 42 | value: { 43 | displayName: 'Text', 44 | type: 'Text', 45 | defaultValue: 'Button', 46 | }, 47 | url: { 48 | displayName: 'URL', 49 | type: 'Text', 50 | defaultValue: '/', 51 | }, 52 | variant: { 53 | displayName: 'Variant', 54 | type: 'Text', 55 | validations: { 56 | in: [ 57 | { value: 'filled', displayName: 'Filled' }, 58 | { value: 'outlined', displayName: 'Outlined' }, 59 | { value: 'gradient', displayName: 'Gradient' }, 60 | { value: 'text', displayName: 'Text' }, 61 | ], 62 | }, 63 | defaultValue: 'filled', 64 | group: 'style', 65 | }, 66 | color: { 67 | displayName: 'Color', 68 | type: 'Text', 69 | validations: { 70 | in: TailwindColors, 71 | }, 72 | defaultValue: 'inherit', 73 | group: 'style', 74 | }, 75 | size: { 76 | displayName: 'Size', 77 | type: 'Text', 78 | validations: { 79 | in: [ 80 | { value: 'sm', displayName: 'Small' }, 81 | { value: 'md', displayName: 'Medium' }, 82 | { value: 'lg', displayName: 'Large' }, 83 | ], 84 | }, 85 | defaultValue: 'md', 86 | group: 'style', 87 | }, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/designSystem/users/login-cards.tsx: -------------------------------------------------------------------------------- 1 | import { getUsers } from '@/mocks'; 2 | import { User } from '@/models/commerce-types'; 3 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 4 | import React from 'react'; 5 | import LoginCard from './login-card'; 6 | 7 | export default function LoginCards(props: any) { 8 | const [users, setUsers] = React.useState>(); 9 | 10 | React.useEffect(() => { 11 | let isMounted = true; 12 | 13 | const loadUsers = async () => { 14 | const mockUsers = await getUsers(); 15 | 16 | if (mockUsers && Array.isArray(mockUsers)) { 17 | const sortedUsers = mockUsers?.sort((a: User, b: User) => { 18 | if (a.country.code > b.country.code) return -1; 19 | if (a.country.code < b.country.code) return 1; 20 | return 0; 21 | }); 22 | if (isMounted) { 23 | setUsers(sortedUsers); 24 | } 25 | } 26 | }; 27 | 28 | loadUsers(); 29 | return () => { 30 | isMounted = false; 31 | }; 32 | }, []); 33 | 34 | return ( 35 |
36 | {users?.map((user: User, key: number) => { 37 | return ; 38 | })} 39 |
40 | ); 41 | } 42 | 43 | export const loginCardsDefinition: ComponentDefinition = { 44 | id: 'login-cards', 45 | name: 'Login Cards', 46 | category: 'Components', 47 | thumbnailUrl: 48 | 'https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600', 49 | tooltip: { 50 | description: 'Displays a card for each demo user account', 51 | }, 52 | builtInStyles: [ 53 | 'cfBackgroundColor', 54 | 'cfBorder', 55 | 'cfBorderRadius', 56 | 'cfFontSize', 57 | 'cfLetterSpacing', 58 | 'cfLineHeight', 59 | 'cfMargin', 60 | 'cfMaxWidth', 61 | 'cfPadding', 62 | 'cfTextAlign', 63 | 'cfTextColor', 64 | 'cfTextTransform', 65 | 'cfWidth', 66 | ], 67 | variables: { 68 | border: { 69 | description: 'Display a border around each login card', 70 | displayName: 'Border', 71 | type: 'Text', 72 | defaultValue: 'false', 73 | group: 'style', 74 | validations: { 75 | in: [ 76 | { displayName: 'True', value: 'true' }, 77 | { displayName: 'False', value: 'false' }, 78 | ], 79 | }, 80 | }, 81 | shadow: { 82 | description: 'Display a drop shadow under each login card', 83 | displayName: 'Shadow', 84 | type: 'Boolean', 85 | defaultValue: false, 86 | group: 'style', 87 | }, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/designSystem/content/widgets/info-widget.tsx: -------------------------------------------------------------------------------- 1 | import { useEditMode } from '@/hooks'; 2 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 3 | import { Typography } from '@material-tailwind/react'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | 7 | export default function InfoWidget(props: any) { 8 | const { editMode } = useEditMode(); 9 | const { linkText, linkURL, ...widgetBodyProps } = props; 10 | 11 | return linkURL ? ( 12 | 13 | 14 | 15 | ) : ( 16 | 17 | ); 18 | } 19 | 20 | export const infoWidgetDefinition: ComponentDefinition = { 21 | id: 'info-widget', 22 | name: 'Info Widget', 23 | category: 'Components', 24 | thumbnailUrl: 25 | 'https://images.ctfassets.net/yv5x7043a54k/UCx5ergDX9fFGaQ5lIqlI/98f1b46796da9fcb736144caa887a331/order_history.svg', 26 | tooltip: { 27 | description: 'Enter your description here', 28 | }, 29 | builtInStyles: [ 30 | 'cfBackgroundColor', 31 | 'cfBorder', 32 | 'cfBorderRadius', 33 | 'cfFontSize', 34 | 'cfLetterSpacing', 35 | 'cfLineHeight', 36 | 'cfMargin', 37 | 'cfMaxWidth', 38 | 'cfPadding', 39 | 'cfTextAlign', 40 | 'cfTextColor', 41 | 'cfTextTransform', 42 | 'cfWidth', 43 | ], 44 | variables: { 45 | icon: { 46 | displayName: 'Icon', 47 | type: 'Media', 48 | group: 'content', 49 | }, 50 | text: { 51 | displayName: 'Text', 52 | type: 'Text', 53 | group: 'content', 54 | }, 55 | linkText: { 56 | displayName: 'LinkText', 57 | type: 'Text', 58 | group: 'content', 59 | }, 60 | linkURL: { 61 | displayName: 'LinkURL', 62 | type: 'Text', 63 | group: 'content', 64 | }, 65 | }, 66 | }; 67 | 68 | const WidgetBody = (props: any) => { 69 | const { icon, text } = props; 70 | 71 | return ( 72 |
73 |
74 | {icon && ( 75 | {`${text} 83 | )} 84 |
85 | {text && ( 86 | 90 | {text} 91 | 92 | )} 93 |
94 | ); 95 | }; 96 | -------------------------------------------------------------------------------- /src/models/content-types.tsx: -------------------------------------------------------------------------------- 1 | import { Asset, EntrySys } from 'contentful'; 2 | import { Document } from '@contentful/rich-text-types'; 3 | 4 | export type ContentfulEntry = { 5 | sys: EntrySys; 6 | fields: any; 7 | }; 8 | 9 | export type ArticleType = { 10 | title: string; 11 | type: string; 12 | image: Asset; 13 | teaser: Document; 14 | body: Document; 15 | faqs: Array; 16 | sources: Document; 17 | relatedArticles: Array; 18 | author: ContentfulEntry; 19 | pubDate: string; 20 | slug: string; 21 | }; 22 | 23 | export type ArticleListType = { 24 | title: string; 25 | entries: Array; 26 | }; 27 | 28 | export type FAQType = { 29 | question: string; 30 | answer: Document; 31 | }; 32 | 33 | export type FAQListType = { 34 | title: string; 35 | entries: Array; 36 | }; 37 | 38 | export type MenuType = { 39 | title: string; 40 | menuItems: Array; 41 | }; 42 | 43 | export type MenuItemType = { 44 | text: string; 45 | url?: string; 46 | icon?: string; 47 | image: Asset; 48 | disallowedRoles?: Array; 49 | openInNewWindow: boolean; 50 | children?: Array; 51 | }; 52 | 53 | export type MockDataType = { 54 | title: string; 55 | dataTable: Document; 56 | }; 57 | 58 | export type ProductListType = { 59 | title: string; 60 | entries: Array; 61 | }; 62 | 63 | export type ProfileType = { 64 | name: string; 65 | avatar: Asset; 66 | firstName: string; 67 | lastName: string; 68 | title: string; 69 | organization: string; 70 | email: string; 71 | socialLinks: Array; 72 | bio: Document; 73 | slug: string; 74 | }; 75 | 76 | export type PromotionType = { 77 | title: string; 78 | summary: string; 79 | description?: Document; 80 | image?: Asset; 81 | ctaText?: string; 82 | ctaUrl?: string; 83 | openInNewWindow?: boolean; 84 | }; 85 | 86 | export type ResourceLabelType = { 87 | key: string; 88 | value: string; 89 | }; 90 | 91 | export type SiteConfigType = { 92 | locale?: string; 93 | siteName?: string; 94 | siteDescription?: string; 95 | siteLogo?: Asset; 96 | copyright?: string; 97 | eyebrowMenu?: Array; 98 | footerMenu1?: Array; 99 | footerMenu2?: Array; 100 | footerMenu3?: Array; 101 | primaryMenu?: Array; 102 | socialMenu?: Array; 103 | }; 104 | 105 | export type TestimonialType = { 106 | internalName: string; 107 | quote: string; 108 | reviewer?: string; 109 | rating?: number; 110 | }; 111 | 112 | export type TestimonialsListType = { 113 | title: string; 114 | entries: Array; 115 | }; 116 | 117 | export type WidgetType = { 118 | title: string; 119 | mockData: Document; 120 | description?: string; 121 | icon?: string; 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/designSystem/picker-options.tsx: -------------------------------------------------------------------------------- 1 | export const CSSColors = [ 2 | { value: 'inherit', displayName: 'inherit' }, 3 | { value: 'black', displayName: 'black' }, 4 | { value: 'white', displayName: 'white' }, 5 | { value: 'slategray', displayName: 'slate gray' }, 6 | { value: 'gray', displayName: 'gray' }, 7 | { value: 'brown', displayName: 'brown' }, 8 | { value: 'pink', displayName: 'pink' }, 9 | { value: 'red', displayName: 'red' }, 10 | { value: 'darkorange', displayName: 'dark orange' }, 11 | { value: 'orange', displayName: 'orange' }, 12 | { value: 'gold', displayName: 'amber' }, 13 | { value: 'yellow', displayName: 'yellow' }, 14 | { value: 'lime', displayName: 'lime' }, 15 | { value: 'lightgreen', displayName: 'light green' }, 16 | { value: 'green', displayName: 'green' }, 17 | { value: 'teal', displayName: 'teal' }, 18 | { value: 'cyan', displayName: 'cyan' }, 19 | { value: 'lightblue', displayName: 'light blue' }, 20 | { value: 'blue', displayName: 'blue' }, 21 | { value: 'indigo', displayName: 'indigo' }, 22 | { value: 'rebeccapurple', displayName: 'dark purple' }, 23 | { value: 'purple', displayName: 'purple' }, 24 | ]; 25 | 26 | export const TailwindColors = [ 27 | { value: 'inherit', displayName: 'inherit' }, 28 | { value: 'black', displayName: 'Black' }, 29 | { value: 'white', displayName: 'White' }, 30 | { value: 'blue-gray', displayName: 'Slate' }, 31 | { value: 'gray', displayName: 'Gray' }, 32 | { value: 'brown', displayName: 'Brown' }, 33 | { value: 'pink', displayName: 'Pink' }, 34 | { value: 'red', displayName: 'Red' }, 35 | { value: 'deep-orange', displayName: 'Deep Orange' }, 36 | { value: 'orange', displayName: 'Orange' }, 37 | { value: 'amber', displayName: 'Amber' }, 38 | { value: 'yellow', displayName: 'Yellow' }, 39 | { value: 'lime', displayName: 'Lime' }, 40 | { value: 'light-green', displayName: 'Light Green' }, 41 | { value: 'green', displayName: 'Green' }, 42 | { value: 'teal', displayName: 'Teal' }, 43 | { value: 'cyan', displayName: 'Cyan' }, 44 | { value: 'light-blue', displayName: 'Light Blue' }, 45 | { value: 'blue', displayName: 'Blue' }, 46 | { value: 'indigo', displayName: 'Indigo' }, 47 | { value: 'deep-purple', displayName: 'Deep Purple' }, 48 | { value: 'purple', displayName: 'Purple' }, 49 | ]; 50 | 51 | export const TextFormats = [ 52 | { value: 'h1', displayName: 'h1' }, 53 | { value: 'h2', displayName: 'h2' }, 54 | { value: 'h3', displayName: 'h3' }, 55 | { value: 'h4', displayName: 'h4' }, 56 | { value: 'h5', displayName: 'h5' }, 57 | { value: 'h6', displayName: 'h6' }, 58 | { value: 'lead', displayName: 'lead' }, 59 | { value: 'parargaph', displayName: 'paragraph' }, 60 | { value: 'small', displayName: 'small' }, 61 | ]; 62 | 63 | export const HeadingFormats = TextFormats.filter((format) => 64 | format.value.startsWith('h') 65 | ); 66 | 67 | export const Icons = []; 68 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export default function Loader() { 6 | return ( 7 |
8 |
9 | 10 | Loading... 11 | 12 | 18 |   19 | 20 | 26 |   27 | 28 | 34 |   35 | 36 | 42 |   43 | 44 | 50 |   51 | 52 | 58 |   59 | 60 | 66 |   67 | 68 |
69 |
70 | 78 | 83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/primary-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon, Menu } from '@/components/designSystem/'; 4 | import { useAppContext, useSiteConfig, useSiteLabels } from '@/hooks'; 5 | import { List, ListItem, ListItemPrefix } from '@material-tailwind/react'; 6 | import { usePathname, useRouter } from 'next/navigation'; 7 | import React from 'react'; 8 | 9 | export default function PrimaryNav(props: any) { 10 | const { closeDrawer } = props; 11 | const { state, logout } = useAppContext(); 12 | const { siteConfig } = useSiteConfig(); 13 | const { siteLabels } = useSiteLabels(); 14 | const pathname = usePathname(); 15 | const router = useRouter(); 16 | 17 | const isActive = pathname.startsWith('/dashboard'); 18 | 19 | const [menuItems, setMenuItems] = React.useState>(); 20 | // const [showLogoutModal, setShowLogoutModal] = React.useState(false); 21 | 22 | React.useEffect(() => { 23 | let isMounted = true; 24 | 25 | if (isMounted) { 26 | const { primaryMenu } = siteConfig; 27 | const primaryMenuItems = primaryMenu?.map((item: any) => { 28 | return item.fields; 29 | }); 30 | 31 | setMenuItems(primaryMenuItems); 32 | } 33 | 34 | return () => { 35 | isMounted = false; 36 | }; 37 | }, [siteConfig]); 38 | 39 | const handleLogout = () => { 40 | logout(); 41 | // setShowLogoutModal(true); 42 | handleLinkClick('/'); 43 | }; 44 | 45 | const handleLinkClick = (href: string) => { 46 | if (href) router.push(href); 47 | closeDrawer(); 48 | }; 49 | 50 | return ( 51 | <> 52 |
53 | 54 | handleLinkClick(state.currentUserRoles[0])} 60 | > 61 | 62 | 63 | 64 | {siteLabels['label.dashboard']} 65 | 66 | 67 | {menuItems && ( 68 | 74 | )} 75 | 76 | 80 | 81 | 82 | 83 | {siteLabels['label.signout']?.toUpperCase()} 84 | 85 | 86 |
87 | {/* {showLogoutModal && } */} 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/designSystem/navigation/menu-item.tsx: -------------------------------------------------------------------------------- 1 | import { getContentfulImageUrl } from '@/utils/image-utils'; 2 | import { 3 | ListItem, 4 | ListItemPrefix, 5 | ListItemSuffix, 6 | } from '@material-tailwind/react'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | 10 | export default function MenuItem(props: any) { 11 | const { 12 | menuitem, 13 | isActive, 14 | menuicons, 15 | iconsize, 16 | fontSize, 17 | handleLinkClick = false, 18 | } = props; 19 | 20 | return menuitem?.url ? ( 21 | handleLinkClick ? ( 22 | 32 | ) : ( 33 | 37 | 40 | 41 | ) 42 | ) : ( 43 | 44 | ); 45 | } 46 | 47 | const MenuItemLabel = (props: any) => { 48 | const { 49 | menuitem, 50 | isActive, 51 | menuicons, 52 | iconsize, 53 | fontSize, 54 | handleLinkClick = false, 55 | } = props; 56 | const text = menuitem?.text; 57 | const icon = menuitem?.icon; 58 | 59 | return ( 60 | handleLinkClick && handleLinkClick(menuitem.url)} 66 | ripple={false} 67 | > 68 | {menuicons === 'left' && ( 69 | 70 | {`${text} 78 | 79 | )} 80 | {menuicons === 'only' ? ( 81 | {`${text} 89 | ) : ( 90 | text 91 | )} 92 | {menuicons === 'right' && ( 93 | 94 | {`${text} 102 | 103 | )} 104 | 105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/designSystem/orders/order-details-header.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const OrderDetailsHeader = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 | {siteLabels['label.orderNumber']} 16 | 17 | 18 | {details.code} 19 | 20 |
21 |
22 | 23 | {siteLabels['label.creationTime']} 24 | 25 | 26 | {localizeDate(state.currentLocale, details.creationTime)} 27 | 28 |
29 |
30 |
31 |
32 | {siteLabels['label.poNumber']} 33 | 34 | {details.purchaseOrderNumber} 35 | 36 |
37 |
38 | {siteLabels['label.costCenter']} 39 | 40 | {details.costCenter?.name} 41 | 42 |
43 |
44 |
45 |
46 | 47 | {siteLabels['label.deliveryAddress']} 48 | 49 | {details.deliveryAddress && ( 50 | 51 | {details.deliveryAddress?.street1} 52 |
53 | {details.deliveryAddress.street2 && 54 | `${details.deliveryAddress.street2}
`} 55 | {`${details.deliveryAddress.city}${ 56 | details.deliveryAddress.stateOrProvince && 57 | `, ${details.deliveryAddress.stateOrProvince}` 58 | } ${details.deliveryAddress.postalcode}`} 59 |
60 | {details.deliveryAddress.countryCode} 61 |
62 | )} 63 |
64 |
65 | 66 | {siteLabels['label.deliveryMode']} 67 | 68 | {details.deliveryMode && ( 69 | 70 | {details.deliveryMode.name} 71 | 72 | )} 73 |
74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/designSystem/quotes/quote-details-header.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const QuoteDetailsHeader = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 | {siteLabels['label.quoteNumber']} 16 | 17 | 18 | {details.code} 19 | 20 |
21 |
22 | 23 | {siteLabels['label.creationTime']} 24 | 25 | 26 | {localizeDate(state.currentLocale, details.creationTime)} 27 | 28 |
29 |
30 |
31 |
32 | {siteLabels['label.poNumber']} 33 | 34 | {details.purchaseOrderNumber} 35 | 36 |
37 |
38 | {siteLabels['label.costCenter']} 39 | 40 | {details.costCenter?.name} 41 | 42 |
43 |
44 |
45 |
46 | 47 | {siteLabels['label.deliveryAddress']} 48 | 49 | {details.deliveryAddress && ( 50 | 51 | {details.deliveryAddress?.street1} 52 |
53 | {details.deliveryAddress.street2 && 54 | `${details.deliveryAddress.street2}
`} 55 | {`${details.deliveryAddress.city}${ 56 | details.deliveryAddress.stateOrProvince && 57 | `, ${details.deliveryAddress.stateOrProvince}` 58 | } ${details.deliveryAddress.postalcode}`} 59 |
60 | {details.deliveryAddress.countryCode} 61 |
62 | )} 63 |
64 |
65 | 66 | {siteLabels['label.deliveryMode']} 67 | 68 | {details.deliveryMode && ( 69 | 70 | {details.deliveryMode.name} 71 | 72 | )} 73 |
74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/designSystem/orders/order-details-footer.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeCurrency } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const OrderDetailsFooter = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 | 17 | {siteLabels['label.orderSummary']} 18 | 19 |
20 | 25 | {siteLabels['label.subtotalAfterDiscounts']} 26 | 27 | 32 | {localizeCurrency(state.currentLocale, details.subTotal.value)} 33 | 34 |
35 |
36 | 41 | {siteLabels['label.shipping']} 42 | 43 | 48 | {localizeCurrency( 49 | state.currentLocale, 50 | details.deliveryMode.deliveryCost.value 51 | )} 52 | 53 |
54 |
55 | 60 | {siteLabels['label.tax']} 61 | 62 | 67 | {localizeCurrency(state.currentLocale, details.totalTax.value)} 68 | 69 |
70 |
71 | 76 | {siteLabels['label.total']} 77 | 78 | 83 | {localizeCurrency(state.currentLocale, details.totalPrice.value)} 84 | 85 |
86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/components/designSystem/quotes/quote-details-footer.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeCurrency } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | export const QuoteDetailsFooter = (props: any) => { 6 | const { details } = props; 7 | const { state } = useAppContext(); 8 | const { siteLabels } = useSiteLabels(); 9 | 10 | return ( 11 |
12 | 17 | {siteLabels['label.quoteSummary']} 18 | 19 |
20 | 25 | {siteLabels['label.subtotalAfterDiscounts']} 26 | 27 | 32 | {localizeCurrency(state.currentLocale, details.subTotal.value)} 33 | 34 |
35 |
36 | 41 | {siteLabels['label.shipping']} 42 | 43 | 48 | {localizeCurrency( 49 | state.currentLocale, 50 | details.deliveryMode.deliveryCost.value 51 | )} 52 | 53 |
54 |
55 | 60 | {siteLabels['label.tax']} 61 | 62 | 67 | {localizeCurrency(state.currentLocale, details.totalTax.value)} 68 | 69 |
70 |
71 | 76 | {siteLabels['label.total']} 77 | 78 | 83 | {localizeCurrency(state.currentLocale, details.totalPrice.value)} 84 | 85 |
86 |
87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /src/mocks/users.tsx: -------------------------------------------------------------------------------- 1 | import { User } from '@/models/commerce-types'; 2 | import { germany, us } from './countries'; 3 | import { euro, usdollar } from './currencies'; 4 | import { english, german } from './languages'; 5 | 6 | export const anna_schmidt: User = { 7 | uid: 'fb2eccd9', 8 | name: 'Anna Schmidt', 9 | firstName: 'Anna', 10 | lastName: 'Schmidt', 11 | email: 'anna.schmidt@bauhaus.de', 12 | currency: euro, 13 | language: german, 14 | country: germany, 15 | userAvatar: { 16 | url: '/images/anna_schmidt.jpg', 17 | }, 18 | orgUnit: '0005678', 19 | roles: ['manager', 'approver'], 20 | }; 21 | 22 | export const david_miller: User = { 23 | uid: '494fdb81', 24 | name: 'David Miller', 25 | firstName: 'David', 26 | lastName: 'Miller', 27 | email: 'dmiller@thediycompany.com', 28 | currency: usdollar, 29 | language: english, 30 | country: us, 31 | userAvatar: { 32 | url: '/images/david_miller.jpg', 33 | }, 34 | orgUnit: '0001234', 35 | roles: ['manager', 'approver'], 36 | }; 37 | 38 | export const emily_johnson: User = { 39 | uid: '4f2ec489', 40 | name: 'Emily Johnson', 41 | firstName: 'Emily', 42 | lastName: 'Johnson', 43 | email: 'ejohnson@thediycompany.com', 44 | currency: usdollar, 45 | language: english, 46 | country: us, 47 | userAvatar: { 48 | url: '/images/emily_johnson.jpg', 49 | }, 50 | orgUnit: '0001234', 51 | roles: ['customer'], 52 | }; 53 | 54 | export const jonas_fischer: User = { 55 | uid: 'ab0b9a97', 56 | name: 'Jonas Fischer', 57 | firstName: 'Jonas', 58 | lastName: 'Fischer', 59 | email: 'jonas.fischer@bauhaus.de', 60 | currency: euro, 61 | language: german, 62 | country: germany, 63 | userAvatar: { 64 | url: '/images/jonas_fischer.jpg', 65 | }, 66 | orgUnit: '0005678', 67 | roles: ['administrator'], 68 | }; 69 | 70 | export const lukas_muller: User = { 71 | uid: 'e0510389', 72 | name: 'Lukas Müller', 73 | firstName: 'Lukas', 74 | lastName: 'Müller', 75 | email: 'lukas.muller@bauhaus.de', 76 | currency: euro, 77 | language: german, 78 | country: germany, 79 | userAvatar: { 80 | url: '/images/lukas_muller.jpg', 81 | }, 82 | orgUnit: '0005678', 83 | roles: ['customer'], 84 | }; 85 | 86 | export const sarah_anderson: User = { 87 | uid: '3855fca3', 88 | name: 'Sarah Anderson', 89 | firstName: 'Sarah', 90 | lastName: 'Anderson', 91 | email: 'sanderson@thediycompany.com', 92 | currency: usdollar, 93 | language: english, 94 | country: us, 95 | userAvatar: { 96 | url: '/images/sarah_anderson.jpg', 97 | }, 98 | orgUnit: '0001234', 99 | roles: ['administrator'], 100 | }; 101 | 102 | export const MockUsers: Array = [ 103 | emily_johnson, 104 | david_miller, 105 | sarah_anderson, 106 | lukas_muller, 107 | anna_schmidt, 108 | jonas_fischer, 109 | ]; 110 | 111 | export const getUser = async (uid: string): Promise => { 112 | if (!uid) return null; 113 | 114 | const mockUser = MockUsers.find((user) => user.uid === uid); 115 | if (!mockUser) return null; 116 | 117 | return mockUser; 118 | }; 119 | 120 | export const getUsers = async (): Promise => { 121 | if (!MockUsers) return null; 122 | return MockUsers; 123 | }; 124 | -------------------------------------------------------------------------------- /src/components/designSystem/content/faqs/faq-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { EditText, FAQ } from '@/components/designSystem'; 3 | import { 4 | TailwindColors, 5 | TextFormats, 6 | } from '@/components/designSystem/picker-options'; 7 | import { useEditMode } from '@/hooks'; 8 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 9 | import React from 'react'; 10 | 11 | export default function FAQList(props: any) { 12 | const { editMode } = useEditMode(); 13 | 14 | const { qcolor, qformat, variant } = props; 15 | 16 | const [entries, setEntries] = React.useState>(); 17 | 18 | React.useEffect(() => { 19 | let isMounted = true; 20 | 21 | const loadEntries = () => { 22 | if (props.entries) { 23 | if (isMounted) { 24 | setEntries(props.entries); 25 | } 26 | } 27 | }; 28 | 29 | loadEntries(); 30 | 31 | return () => { 32 | isMounted = false; 33 | }; 34 | }, [props]); 35 | 36 | return ( 37 | <> 38 | {!entries ? ( 39 | editMode && 40 | ) : ( 41 |
42 | {entries?.map((faq: any, key: number) => { 43 | const faqProps = { 44 | answer: faq?.fields.answer, 45 | pos: key + 1, 46 | question: faq?.fields.question, 47 | qcolor, 48 | qformat, 49 | variant, 50 | }; 51 | return ; 52 | })} 53 |
54 | )} 55 | 56 | ); 57 | } 58 | 59 | export const faqListDefinition: ComponentDefinition = { 60 | id: 'faq-list', 61 | name: 'FAQ List', 62 | category: 'Components', 63 | thumbnailUrl: 64 | 'https://images.ctfassets.net/yv5x7043a54k/sbdQDrIA0WDnBxgq1cAa1/b9c2961c2be22f0a86171bfa0902ff4f/faq_list.svg', 65 | tooltip: { 66 | description: 'Displays a list of FAQs', 67 | }, 68 | builtInStyles: [ 69 | 'cfBackgroundColor', 70 | 'cfBorder', 71 | 'cfBorderRadius', 72 | 'cfFontSize', 73 | 'cfLetterSpacing', 74 | 'cfLineHeight', 75 | 'cfMargin', 76 | 'cfMaxWidth', 77 | 'cfPadding', 78 | 'cfTextAlign', 79 | 'cfTextColor', 80 | 'cfTextTransform', 81 | 'cfWidth', 82 | ], 83 | variables: { 84 | entries: { 85 | displayName: 'Entries', 86 | type: 'Array', 87 | group: 'content', 88 | }, 89 | variant: { 90 | displayName: 'Variant', 91 | type: 'Text', 92 | group: 'style', 93 | defaultValue: 'accordion', 94 | validations: { 95 | in: [ 96 | { displayName: 'Accordion', value: 'accordion' }, 97 | { displayName: 'Paragraph', value: 'paragraph' }, 98 | ], 99 | }, 100 | }, 101 | qcolor: { 102 | displayName: 'Question Color', 103 | type: 'Text', 104 | group: 'style', 105 | defaultValue: 'inherit', 106 | validations: { 107 | in: [{ displayName: 'inherit', value: 'inherit' }, ...TailwindColors], 108 | }, 109 | }, 110 | qformat: { 111 | displayName: 'Question Format', 112 | type: 'Text', 113 | group: 'style', 114 | defaultValue: 'h5', 115 | validations: { 116 | in: TextFormats, 117 | }, 118 | }, 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /src/components/designSystem/tickets/ticket-details-events.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { localizeDate } from '@/utils/locale-utils'; 3 | import { Typography } from '@material-tailwind/react'; 4 | 5 | const TICKET_EVENT_ATTRIBUTES = [ 6 | 'code', 7 | 'author', 8 | 'createdAt', 9 | 'message', 10 | 'addedByAgent', 11 | 'toStatus', 12 | ]; 13 | 14 | export const TicketDetailsEvents = (props: any) => { 15 | const { details } = props; 16 | const { siteLabels } = useSiteLabels(); 17 | 18 | return ( 19 |
20 |
21 | 22 | {siteLabels['label.ticketEvents']} 23 | 24 |
25 |
26 | 27 | {details.ticketEvents.map((event: any) => { 28 | return ; 29 | })} 30 |
31 |
32 | ); 33 | }; 34 | 35 | const TicketDetailsEventLabels = (props: any) => { 36 | const { siteLabels } = useSiteLabels(); 37 | 38 | return ( 39 |
40 | {TICKET_EVENT_ATTRIBUTES.map((label: string, key: number) => { 41 | return ( 42 | 48 | {siteLabels[`label.${label}`]} 49 | 50 | ); 51 | })} 52 |
53 | ); 54 | }; 55 | 56 | const TicketDetailsEvent = (props: any) => { 57 | const { event } = props; 58 | const { state } = useAppContext(); 59 | const { siteLabels } = useSiteLabels(); 60 | 61 | return ( 62 |
63 | 68 | {event.code} 69 | 70 | 75 | {event.author} 76 | 77 | 82 | {localizeDate(state.currentLocale, event.createdAt)} 83 | 84 |
89 | 94 | {event.addedByAgent ? 'true' : 'false'} 95 | 96 | 101 | {siteLabels['label.' + event.toStatus]} 102 | 103 |
104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/sign-in.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { useAppContext } from '@/hooks'; 3 | import { getUsers } from '@/mocks'; 4 | import { User } from '@/models/commerce-types'; 5 | import { 6 | Avatar, 7 | Button, 8 | Menu, 9 | MenuHandler, 10 | MenuItem, 11 | MenuList, 12 | Typography, 13 | } from '@material-tailwind/react'; 14 | import { useRouter } from 'next/navigation'; 15 | import React from 'react'; 16 | 17 | export default function SignIn(props: any) { 18 | const { state, updateState } = useAppContext(); 19 | const router = useRouter(); 20 | 21 | const [open, setOpen] = React.useState(false); 22 | const [users, setUsers] = React.useState>(); 23 | 24 | React.useEffect(() => { 25 | let isMounted = true; 26 | 27 | const loadUsers = async () => { 28 | const mockUsers = await getUsers(); 29 | 30 | if (mockUsers && Array.isArray(mockUsers)) { 31 | const sortedUsers = mockUsers?.sort((a: User, b: User) => { 32 | if (a.country.code > b.country.code) return -1; 33 | if (a.country.code < b.country.code) return 1; 34 | return 0; 35 | }); 36 | if (isMounted) { 37 | setUsers(sortedUsers); 38 | } 39 | } 40 | }; 41 | 42 | loadUsers(); 43 | return () => { 44 | isMounted = false; 45 | }; 46 | }, []); 47 | 48 | const handleLogin = (user: User) => { 49 | const newState = { 50 | currentLocale: user.language.isocode + '-' + user.country.code, 51 | currentUser: user.uid, 52 | currentUserRoles: user.roles, 53 | currentOrgUnit: user.orgUnit, 54 | }; 55 | updateState(newState); 56 | router.push(`/${user.roles[0]}`); 57 | }; 58 | 59 | const toggleOpen = () => { 60 | setOpen((open) => { 61 | return !open; 62 | }); 63 | }; 64 | 65 | return ( 66 | <> 67 | {users && ( 68 | 74 | 75 | 81 | 82 | 83 | {users?.map((user: User, key: number) => { 84 | return ( 85 | handleLogin(user)} 89 | > 90 | 91 | 92 | ); 93 | })} 94 | 95 | 96 | )} 97 | 98 | ); 99 | } 100 | 101 | const SignInUser = (user: User) => { 102 | return ( 103 |
104 | 105 |
106 | 107 | {user.firstName} 108 | 109 | 113 | {user.country.code} {user.roles?.[0]} 114 | 115 |
116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/designSystem/users/profile-details-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@/components/designSystem'; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogBody, 6 | DialogFooter, 7 | DialogHeader, 8 | Typography, 9 | } from '@material-tailwind/react'; 10 | import Image from 'next/image'; 11 | 12 | export default function ProfileDetailsModal(props: any) { 13 | const { 14 | handleCloseProfile, 15 | handleEditProfile, 16 | handleOpenProfile, 17 | showProfile, 18 | siteLabels, 19 | user, 20 | } = props; 21 | 22 | return ( 23 | 29 | 30 | {siteLabels['label.userProfile']} 31 | 32 | 33 |
34 |
35 | avatar 43 |
44 |
45 |
46 | 52 | {user?.name} 53 | 54 | 60 | {user?.orgUnit} 61 | 62 | 68 | {user?.roles 69 | .map((role: string) => { 70 | return siteLabels['label.' + role.toLowerCase()]; 71 | }) 72 | .join(', ')} 73 | 74 |
75 | 76 |
77 | 78 |
79 | 80 | 81 | {user?.email} 82 | 83 | 84 | 85 | {user?.language.isocode}-{user?.country.code} 86 | 87 |
88 |
89 |
90 |
91 | 92 | 95 | 98 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/studio-config.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { defineComponents } from '@contentful/experiences-sdk-react'; 3 | import { Alert, Avatar } from '@material-tailwind/react'; 4 | import { 5 | alertDefinition, 6 | Article, 7 | ArticleCard, 8 | articleCardDefinition, 9 | articleDefinition, 10 | ArticleList, 11 | articleListDefinition, 12 | avatarDefinition, 13 | Breadcrumbs, 14 | breadcrumbsDefinition, 15 | Button, 16 | buttonDefinition, 17 | DataTable, 18 | dataTableDefinition, 19 | DataWidget, 20 | dataWidgetDefinition, 21 | FAQ, 22 | faqDefinition, 23 | FAQList, 24 | faqListDefinition, 25 | Heading, 26 | headingDefinition, 27 | Hero, 28 | heroDefinition, 29 | InfoWidget, 30 | infoWidgetDefinition, 31 | LoginCard, 32 | loginCardDefinition, 33 | Menu, 34 | menuDefinition, 35 | OrderHistory, 36 | orderHistoryDefinition, 37 | ProductCollection, 38 | productCollectionDefinition, 39 | ProductDetails, 40 | productDetailsDefinition, 41 | PromoCard, 42 | promoCardDefinition, 43 | QuoteHistory, 44 | quoteHistoryDefinition, 45 | Rating, 46 | ratingDefinition, 47 | SearchResults, 48 | searchResultsDefinition, 49 | Testimonial, 50 | testimonialDefinition, 51 | TicketHistory, 52 | ticketHistoryDefinition, 53 | } from './designSystem'; 54 | 55 | defineComponents([ 56 | { 57 | component: Alert, 58 | definition: alertDefinition, 59 | }, 60 | { 61 | component: Article, 62 | definition: articleDefinition, 63 | }, 64 | { 65 | component: ArticleCard, 66 | definition: articleCardDefinition, 67 | }, 68 | { 69 | component: ArticleList, 70 | definition: articleListDefinition, 71 | }, 72 | { 73 | component: Avatar, 74 | definition: avatarDefinition, 75 | }, 76 | { 77 | component: Breadcrumbs, 78 | definition: breadcrumbsDefinition, 79 | }, 80 | { 81 | component: Button, 82 | definition: buttonDefinition, 83 | }, 84 | { 85 | component: DataTable, 86 | definition: dataTableDefinition, 87 | }, 88 | { 89 | component: DataWidget, 90 | definition: dataWidgetDefinition, 91 | }, 92 | { 93 | component: FAQ, 94 | definition: faqDefinition, 95 | }, 96 | { 97 | component: FAQList, 98 | definition: faqListDefinition, 99 | }, 100 | { 101 | component: Heading, 102 | definition: headingDefinition, 103 | }, 104 | { 105 | component: Hero, 106 | definition: heroDefinition, 107 | }, 108 | { 109 | component: InfoWidget, 110 | definition: infoWidgetDefinition, 111 | }, 112 | { 113 | component: LoginCard, 114 | definition: loginCardDefinition, 115 | }, 116 | { 117 | component: Menu, 118 | definition: menuDefinition, 119 | }, 120 | { 121 | component: OrderHistory, 122 | definition: orderHistoryDefinition, 123 | }, 124 | { 125 | component: ProductCollection, 126 | definition: productCollectionDefinition, 127 | }, 128 | { 129 | component: ProductDetails, 130 | definition: productDetailsDefinition, 131 | }, 132 | { 133 | component: PromoCard, 134 | definition: promoCardDefinition, 135 | }, 136 | { 137 | component: QuoteHistory, 138 | definition: quoteHistoryDefinition, 139 | }, 140 | { 141 | component: Rating, 142 | definition: ratingDefinition, 143 | }, 144 | { 145 | component: SearchResults, 146 | definition: searchResultsDefinition, 147 | }, 148 | { 149 | component: TicketHistory, 150 | definition: ticketHistoryDefinition, 151 | }, 152 | { 153 | component: Testimonial, 154 | definition: testimonialDefinition, 155 | }, 156 | ]); 157 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/locale-selector.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon } from '@/components/designSystem'; 4 | import useAppContext from '@/hooks/app-context'; 5 | import { MockCountries as countries } from '@/mocks'; 6 | import { Country } from '@/models/commerce-types'; 7 | import { 8 | Menu, 9 | MenuHandler, 10 | MenuItem, 11 | MenuList, 12 | Typography, 13 | } from '@material-tailwind/react'; 14 | import Image from 'next/image'; 15 | import React from 'react'; 16 | 17 | export default function LocaleSelector(props: any) { 18 | const { state, updateState } = useAppContext(); 19 | const { currentLocale } = state; 20 | const { variant } = props; 21 | 22 | const [selected, setSelected] = React.useState(); 23 | 24 | React.useEffect(() => { 25 | const [isocode, countrycode] = state.currentLocale.split('-'); 26 | const country = countries.find( 27 | (cntry: Country) => 28 | cntry.languages[0].isocode === isocode && cntry.code === countrycode 29 | ); 30 | 31 | if (country) setSelected(country); 32 | }, [state]); 33 | 34 | const handleClick = (value: any) => { 35 | const newState = { ...state, currentLocale: value }; 36 | updateState(newState); 37 | }; 38 | 39 | return ( 40 |
41 | 42 | 43 |
44 |
45 | {selected?.flags?.svg && ( 46 | {`${selected?.name} 54 | )} 55 | 60 | {selected?.name} [{state.currentLocale}] 61 | 62 | 63 |
64 |
65 |
66 | 67 | {countries?.map( 68 | ({ code, flags, languages, name }: Partial) => ( 69 | handleClick(`${languages?.[0].isocode}-${code}`)} 72 | className='flex gap-2 items-center w-full' 73 | > 74 |
75 | {flags?.svg && ( 76 | {`${name} 84 | )} 85 | 89 | {`${name} [${languages?.[0].isocode}-${code}]`} 90 | 91 |
92 |
93 | ) 94 | )} 95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/components/designSystem/shared/pagination.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon } from '@/components/designSystem'; 4 | import { Button, ButtonGroup } from '@material-tailwind/react'; 5 | import React from 'react'; 6 | 7 | const MAX_BUTTONS = 5; 8 | 9 | export default function Pagination(props: any) { 10 | const [buttons, setButtons] = React.useState(); 11 | const { handleChangePage, pagination } = props; 12 | 13 | React.useEffect(() => { 14 | let isMounted = true; 15 | 16 | const loadButtons = () => { 17 | const currentPageNumber = pagination.currentPage + 1; 18 | const btnCount = 19 | pagination.totalPages <= MAX_BUTTONS 20 | ? pagination.totalPages 21 | : MAX_BUTTONS - 2; 22 | const numberedButtons = new Array(btnCount); 23 | 24 | const start = 25 | pagination.totalPages <= MAX_BUTTONS 26 | ? 1 27 | : currentPageNumber < btnCount 28 | ? 1 29 | : currentPageNumber === pagination.totalPages 30 | ? pagination.totalPages + 1 - btnCount 31 | : currentPageNumber - 1; 32 | 33 | for (var i = start; i < start + btnCount; i++) { 34 | numberedButtons.push({ label: `${i}`, value: i - 1 }); 35 | } 36 | 37 | if (isMounted) { 38 | setButtons(numberedButtons); 39 | } 40 | }; 41 | 42 | loadButtons(); 43 | 44 | return () => { 45 | isMounted = false; 46 | }; 47 | }, [pagination]); 48 | 49 | const isCurrentPage = (page: number): boolean => { 50 | return pagination.currentPage === page; 51 | }; 52 | 53 | const isFirstPage = pagination.currentPage === 0; 54 | const isLastPage = pagination.currentPage === pagination.totalPages - 1; 55 | 56 | const firstPageButton = ( 57 | 66 | ); 67 | 68 | const lastPageButton = ( 69 | 78 | ); 79 | 80 | return ( 81 |
82 | 83 | {firstPageButton} 84 | 85 | {buttons?.map((button: any, key: number) => { 86 | let className = 87 | 'flex font-normal h-12 items-center justify-center text-md w-12'; 88 | 89 | if (isCurrentPage(button.value)) { 90 | className = className + ' bg-blue-900 text-white'; 91 | } else { 92 | className = className + ' hover:bg-gray-200'; 93 | } 94 | if (pagination.totalPages <= MAX_BUTTONS && key == 0) { 95 | className = className + ' border rounded-md'; 96 | } 97 | 98 | return ( 99 | 109 | ); 110 | })} 111 | 112 | {lastPageButton} 113 | 114 |
115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/mocks/tickets.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DataTableColumn, 3 | Ticket, 4 | TicketTableData, 5 | } from '@/models/commerce-types'; 6 | 7 | export const TICKET_DATA_COLS: Array = [ 8 | { key: 'code', label: 'ID', format: 'link' }, 9 | { key: 'status', label: 'Status', format: 'label' }, 10 | { key: 'subject', label: 'Subject', format: 'text' }, 11 | { key: 'creationTime', label: 'Created', format: 'datetime' }, 12 | { key: 'updateTime', label: 'Updated', format: 'datetime' }, 13 | { key: 'guid', label: 'User', format: 'text' }, 14 | ]; 15 | 16 | export const getTicket = async (id: string) => { 17 | return MockTickets.find((ticket) => ticket.id === id); 18 | }; 19 | 20 | export const getTicketsByUser = async ( 21 | guid: string, 22 | locale: string, 23 | tableData: boolean = false 24 | ): Promise | null> => { 25 | if (!guid || (tableData && !locale)) return null; 26 | 27 | const tickets = tableData 28 | ? getTicketsTableData(locale, 'guid', guid) 29 | : getTickets('guid', guid); 30 | 31 | if (!tickets) return null; 32 | 33 | return tickets; 34 | }; 35 | 36 | export const getTickets = async ( 37 | key: string, 38 | value: string 39 | ): Promise | null> => { 40 | if (!(key && value)) return null; 41 | 42 | const tickets = MockTickets.filter((quote: any) => quote[key] === value).sort( 43 | (a: any, b: any) => { 44 | if (a.createdDate > b.createdDate) return 1; 45 | if (a.createdDate < b.createdDate) return -1; 46 | return 0; 47 | } 48 | ); 49 | 50 | if (!tickets) return null; 51 | 52 | return tickets; 53 | }; 54 | 55 | export const getTicketsTableData = async ( 56 | locale: string, 57 | key: string, 58 | value: string 59 | ): Promise | null> => { 60 | if (!(key && value)) return null; 61 | 62 | const tickets = MockTickets.filter( 63 | (ticket: any) => ticket[key] === value 64 | ).map((ticket: any) => { 65 | return { 66 | code: ticket.id, 67 | guid: ticket.customerId, 68 | subject: ticket.subject, 69 | creationTime: ticket.createdAt, 70 | updateTime: ticket.modifiedAt, 71 | status: ticket.status, 72 | ticketCategory: ticket.ticketCategory, 73 | }; 74 | }); 75 | 76 | if (!tickets) return null; 77 | return tickets; 78 | }; 79 | 80 | export const MockTickets = [ 81 | { 82 | id: '0005678', 83 | customerId: 'e0510389', 84 | subject: 'Beim Versand beschädigte Artikel', 85 | createdAt: '2025-01-15T20:29:09.386Z', 86 | modifiedAt: '2025-01-18T20:29:09.386Z', 87 | status: 'open', 88 | ticketEvents: [ 89 | { 90 | code: '0001234', 91 | author: 'e0510389', 92 | createdAt: '2025-01-15T20:29:09.386Z', 93 | message: 'ticket created', 94 | addedByAgent: false, 95 | toStatus: 'open', 96 | }, 97 | { 98 | code: '0002345', 99 | author: 'e0510389', 100 | createdAt: '2025-01-18T20:29:09.386Z', 101 | message: 'ticket updated', 102 | addedByAgent: false, 103 | toStatus: 'open', 104 | }, 105 | ], 106 | ticketCategory: 'orders', 107 | }, 108 | { 109 | id: '0001234', 110 | customerId: '4f2ec489', 111 | subject: 'Items missing from shipment', 112 | createdAt: '2025-01-15T20:29:09.386Z', 113 | modifiedAt: '2025-01-18T20:29:09.386Z', 114 | status: 'open', 115 | ticketEvents: [ 116 | { 117 | code: '0001234', 118 | author: '4f2ec489', 119 | createdAt: '2025-01-15T20:29:09.386Z', 120 | message: 'ticket created', 121 | addedByAgent: false, 122 | toStatus: 'open', 123 | }, 124 | { 125 | code: '0002345', 126 | author: '4f2ec489', 127 | createdAt: '2025-01-18T20:29:09.386Z', 128 | message: 'ticket updated', 129 | addedByAgent: false, 130 | toStatus: 'open', 131 | }, 132 | ], 133 | ticketCategory: 'returns', 134 | }, 135 | ]; 136 | -------------------------------------------------------------------------------- /src/components/designSystem/data-table/table-body.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { DataTableColumn } from '@/models/commerce-types'; 3 | import { localizeCurrency, localizeDate } from '@/utils/locale-utils'; 4 | import { TailwindBgColorsMap } from '@/utils/tailwind-colors-utils'; 5 | import Link from 'next/link'; 6 | import { PREVIEW_COLS, PREVIEW_ROWS } from './data-table'; 7 | 8 | export default function TableBody(props: any) { 9 | const { 10 | cellpadding = 'py-2', 11 | cols, 12 | data, 13 | handleOpenDetails, 14 | headbg, 15 | locale, 16 | editMode, 17 | siteLabels, 18 | } = props; 19 | 20 | let bgcolor; 21 | if ( 22 | !['black', 'gray', 'lime', 'white', 'yellow', 'inherit'].includes(headbg) 23 | ) { 24 | bgcolor = headbg + '-' + 50; 25 | } else { 26 | bgcolor = ['lime', 'yellow'].includes(headbg) 27 | ? headbg + '-100' 28 | : 'gray-100'; 29 | } 30 | 31 | return ( 32 | 33 | {editMode && } 34 | {data && ( 35 | 46 | )} 47 | 48 | ); 49 | } 50 | 51 | const PreviewBody = (props: any) => { 52 | const { bgcolor, cellpadding } = props; 53 | return PREVIEW_ROWS.map((row, key) => { 54 | return ( 55 | 59 | {PREVIEW_COLS.map((col, key2) => { 60 | return ( 61 | 67 | {row} 68 | {col !== '' && ':' + col} 69 | 70 | ); 71 | })} 72 | 73 | ); 74 | }); 75 | }; 76 | 77 | const DataBody = (props: any) => { 78 | const { 79 | bgcolor, 80 | cellpadding, 81 | cols, 82 | data, 83 | handleOpenDetails, 84 | locale, 85 | siteLabels, 86 | } = props; 87 | 88 | return ( 89 | Array.isArray(data) && 90 | data?.map((tableData: Record, key: number) => { 91 | return ( 92 | 96 | {cols?.map((col: DataTableColumn, key2: number) => { 97 | let val; 98 | switch (col.format) { 99 | case 'currency': 100 | val = localizeCurrency(locale, tableData[col.key]); 101 | break; 102 | case 'datetime': 103 | val = localizeDate(locale, tableData[col.key]); 104 | break; 105 | case 'label': 106 | val = siteLabels[`label.${tableData[col.key]}`]?.toLowerCase(); 107 | break; 108 | case 'link': 109 | val = ( 110 | handleOpenDetails(tableData[col.key])} 114 | > 115 | {tableData[col.key]} 116 | 117 | ); 118 | break; 119 | default: 120 | val = tableData[col.key]; 121 | } 122 | return ( 123 | 127 | {val} 128 | 129 | ); 130 | })} 131 | 132 | ); 133 | }) 134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /src/components/designSystem/content/testimonial.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { EditText, Rating } from '@/components/designSystem'; 3 | import { useAppContext, useEditMode } from '@/hooks'; 4 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 5 | import { 6 | Avatar, 7 | Card, 8 | CardBody, 9 | CardHeader, 10 | Typography, 11 | } from '@material-tailwind/react'; 12 | 13 | export default function Testimonial(props: any) { 14 | const { editMode } = useEditMode(); 15 | 16 | const { quote, rating, border, shadow } = props; 17 | const reviewer = props.reviewer?.fields; 18 | const avatarUrl = reviewer?.avatar?.fields?.file?.url; 19 | 20 | return ( 21 | <> 22 | {!(quote || reviewer || rating) ? ( 23 | editMode && 24 | ) : ( 25 | 32 | 38 | 44 |
45 |
46 | 47 | {reviewer?.firstName} {reviewer?.lastName} 48 | 49 |
50 | 51 |
52 |
53 | 54 | {reviewer?.title} @ {reviewer?.organization} 55 | 56 |
57 |
58 | 59 | "{quote}" 60 | 61 |
62 | )} 63 | 64 | ); 65 | } 66 | 67 | export const testimonialDefinition: ComponentDefinition = { 68 | id: 'testimonial', 69 | name: 'Testimonial', 70 | category: 'Components', 71 | thumbnailUrl: 72 | 'https://images.ctfassets.net/yv5x7043a54k/13PExzxKK7tECtdLRlI5vM/f37a286e71fdcaa1a0b2dc6d24cd25af/testimonial.svg', 73 | tooltip: { 74 | description: 'A card displaying a testimonial', 75 | }, 76 | builtInStyles: [ 77 | 'cfBackgroundColor', 78 | 'cfBorder', 79 | 'cfBorderRadius', 80 | 'cfFontSize', 81 | 'cfLetterSpacing', 82 | 'cfLineHeight', 83 | 'cfMargin', 84 | 'cfMaxWidth', 85 | 'cfPadding', 86 | 'cfTextAlign', 87 | 'cfTextColor', 88 | 'cfTextTransform', 89 | 'cfWidth', 90 | ], 91 | variables: { 92 | reviewer: { 93 | description: 'The reviewer from the testimonial content item', 94 | displayName: 'Reviewer', 95 | type: 'Link', 96 | }, 97 | rating: { 98 | description: 'The rating from the testimonail content item', 99 | displayName: 'Rating', 100 | type: 'Number', 101 | defaultValue: 0.0, 102 | }, 103 | quote: { 104 | description: 'The quote from the testimonial content item', 105 | displayName: 'Quote', 106 | type: 'Text', 107 | }, 108 | border: { 109 | description: 'Display a border around the testimonial card', 110 | displayName: 'Border', 111 | type: 'Text', 112 | defaultValue: 'false', 113 | group: 'style', 114 | validations: { 115 | in: [ 116 | { displayName: 'True', value: 'true' }, 117 | { displayName: 'False', value: 'false' }, 118 | ], 119 | }, 120 | }, 121 | shadow: { 122 | description: 'Display a drop shadow under the testimonial card', 123 | displayName: 'Shadow', 124 | type: 'Boolean', 125 | defaultValue: false, 126 | group: 'style', 127 | }, 128 | }, 129 | }; 130 | -------------------------------------------------------------------------------- /src/components/designSystem/content/hero.tsx: -------------------------------------------------------------------------------- 1 | import { EditText } from '@/components/designSystem'; 2 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 3 | import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; 4 | import { Button, Typography } from '@material-tailwind/react'; 5 | import Image from 'next/image'; 6 | import Link from 'next/link'; 7 | 8 | export default function Hero(props: any) { 9 | const { 10 | heading, 11 | description, 12 | ctaText, 13 | ctaURL, 14 | image, 15 | imageAlt, 16 | size, 17 | variant, 18 | } = props; 19 | const headingSize = size === 'sm' ? 'h3' : size === 'md' ? 'h2' : 'h1'; 20 | 21 | return ( 22 |
23 | {!(heading || description || ctaText || ctaURL || image) ? ( 24 | 25 | ) : ( 26 | <> 27 |
28 |
29 | {heading && ( 30 | 35 | {heading} 36 | 37 | )} 38 |
39 | {documentToReactComponents(description)} 40 |
41 | 42 | 45 | 46 |
47 |
48 |
49 | {imageAlt} 57 |
58 | 59 | )} 60 |
61 | ); 62 | } 63 | 64 | export const heroDefinition: ComponentDefinition = { 65 | id: 'hero', 66 | name: 'Hero', 67 | category: 'Components', 68 | thumbnailUrl: 69 | 'https://images.ctfassets.net/yv5x7043a54k/7BAMY84bQmQAKj6i88nGsZ/05c3a892a1bfc517f82f7bf4ab7523eb/hero.svg', 70 | tooltip: { 71 | description: 72 | 'This is a prescriptive hero made up of designs system elements.', 73 | }, 74 | builtInStyles: [ 75 | 'cfBackgroundColor', 76 | 'cfBorder', 77 | 'cfBorderRadius', 78 | 'cfFontSize', 79 | 'cfLetterSpacing', 80 | 'cfLineHeight', 81 | 'cfMargin', 82 | 'cfMaxWidth', 83 | 'cfPadding', 84 | 'cfTextAlign', 85 | 'cfTextColor', 86 | 'cfTextTransform', 87 | 'cfWidth', 88 | ], 89 | variables: { 90 | heading: { 91 | displayName: 'Heading', 92 | type: 'Text', 93 | }, 94 | description: { 95 | displayName: 'Description', 96 | type: 'RichText', 97 | }, 98 | ctaText: { 99 | displayName: 'CTA Text', 100 | type: 'Text', 101 | }, 102 | ctaURL: { 103 | displayName: 'CTA URL', 104 | type: 'Text', 105 | }, 106 | image: { 107 | displayName: 'Background Image', 108 | type: 'Media', 109 | defaultValue: 110 | 'https://images.ctfassets.net/tofsejyzyo24/5owPX1vp6cXDZr7QOabwzT/d5580f5b4dbad3f74c87ce2f03efa581/Image_container.png', 111 | }, 112 | size: { 113 | displayName: 'Size', 114 | type: 'Text', 115 | validations: { 116 | in: [ 117 | { value: 'sm', displayName: 'Small' }, 118 | { value: 'md', displayName: 'Medium' }, 119 | { value: 'lg', displayName: 'Large' }, 120 | ], 121 | }, 122 | defaultValue: 'lg', 123 | group: 'style', 124 | }, 125 | variant: { 126 | displayName: 'Variant', 127 | type: 'Text', 128 | validations: { 129 | in: [ 130 | { value: 'amber', displayName: 'Amber' }, 131 | { value: 'black', displayName: 'Black' }, 132 | { value: 'red', displayName: 'Red' }, 133 | { value: 'white', displayName: 'White' }, 134 | ], 135 | }, 136 | defaultValue: 'black', 137 | group: 'style', 138 | }, 139 | }, 140 | }; 141 | -------------------------------------------------------------------------------- /src/components/designSystem/content/faqs/faq.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { EditText } from '@/components/designSystem'; 3 | import { 4 | TailwindColors, 5 | TextFormats, 6 | } from '@/components/designSystem/picker-options'; 7 | import { useEditMode, useSiteLabels } from '@/hooks'; 8 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 9 | import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; 10 | import { 11 | Accordion, 12 | AccordionBody, 13 | AccordionHeader, 14 | Typography, 15 | } from '@material-tailwind/react'; 16 | import React from 'react'; 17 | 18 | export default function FAQ(props: any) { 19 | const { editMode } = useEditMode(); 20 | const { siteLabels } = useSiteLabels(); 21 | 22 | const { answer, pos = 1, qcolor, qformat, question, variant } = props; 23 | 24 | const [open, setOpen] = React.useState(0); 25 | 26 | const handleOpen = (idx: number) => setOpen(open === idx ? 0 : idx); 27 | const hasContent = question && answer; 28 | 29 | return ( 30 | <> 31 | {!hasContent ? ( 32 | editMode && 33 | ) : variant === 'accordion' ? ( 34 | 35 | handleOpen(pos)} 38 | > 39 | 40 | {question} 41 | 42 | 43 | 44 | 45 | {documentToReactComponents(answer)} 46 | 47 | 48 | 49 | ) : ( 50 |
51 |
52 | 53 | {siteLabels['label.question']?.substring(0, 1)}: 54 | 55 | 56 | {question} 57 | 58 |
59 |
60 | 61 | {siteLabels['label.answer']?.substring(0, 1)}: 62 | 63 | 64 | {documentToReactComponents(answer)} 65 | 66 |
67 |
68 | )} 69 | 70 | ); 71 | } 72 | 73 | export const faqDefinition: ComponentDefinition = { 74 | id: 'faq', 75 | name: 'FAQ', 76 | category: 'Components', 77 | thumbnailUrl: 78 | 'https://images.ctfassets.net/yv5x7043a54k/DPUKWbm9BMHwkZY9q0Cx4/da26f1b2c12b1e2dfa7479d2eb4f76bf/faq.svg', 79 | tooltip: { 80 | description: 'frequently asked question component', 81 | }, 82 | builtInStyles: [ 83 | 'cfBackgroundColor', 84 | 'cfBorder', 85 | 'cfBorderRadius', 86 | 'cfFontSize', 87 | 'cfLetterSpacing', 88 | 'cfLineHeight', 89 | 'cfMargin', 90 | 'cfMaxWidth', 91 | 'cfPadding', 92 | 'cfTextAlign', 93 | 'cfTextColor', 94 | 'cfTextTransform', 95 | 'cfWidth', 96 | ], 97 | variables: { 98 | question: { 99 | displayName: 'Question', 100 | type: 'Text', 101 | }, 102 | answer: { 103 | displayName: 'Answer', 104 | type: 'RichText', 105 | }, 106 | variant: { 107 | displayName: 'Variant', 108 | type: 'Text', 109 | group: 'style', 110 | defaultValue: 'accordion', 111 | validations: { 112 | in: [ 113 | { displayName: 'Accordion', value: 'accordion' }, 114 | { displayName: 'Paragraph', value: 'paragraph' }, 115 | ], 116 | }, 117 | }, 118 | qcolor: { 119 | displayName: 'Question Color', 120 | type: 'Text', 121 | group: 'style', 122 | defaultValue: 'inherit', 123 | validations: { 124 | in: [{ displayName: 'inherit', value: 'inherit' }, ...TailwindColors], 125 | }, 126 | }, 127 | qformat: { 128 | displayName: 'Question Format', 129 | type: 'Text', 130 | group: 'style', 131 | defaultValue: 'h6', 132 | validations: { 133 | in: TextFormats, 134 | }, 135 | }, 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/components/designSystem/users/login-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAppContext } from '@/hooks'; 4 | import { User } from '@/models/commerce-types'; 5 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 6 | import { 7 | Button, 8 | Card, 9 | CardBody, 10 | CardFooter, 11 | CardHeader, 12 | Typography, 13 | } from '@material-tailwind/react'; 14 | import Image from 'next/image'; 15 | import { useRouter } from 'next/navigation'; 16 | 17 | export default function LoginCard(props: any) { 18 | const router = useRouter(); 19 | const appCtx = useAppContext(); 20 | const { updateState } = appCtx; 21 | const { user } = props; 22 | 23 | const handleClick = (user: User) => { 24 | const newState = { 25 | currentLocale: user.language.isocode + '-' + user.country.code, 26 | currentUser: user.uid, 27 | currentUserRoles: user.roles, 28 | currentOrgUnit: user.orgUnit, 29 | }; 30 | updateState(newState); 31 | router.push(`/${user.roles[0]}`); 32 | }; 33 | 34 | return ( 35 | 40 | 41 | {user.userAvatar?.url && ( 42 | avatar 50 | )} 51 | 52 | 53 |
54 | 55 | {user.firstName} {user.lastName} 56 | 57 | {user.country.code && ( 58 | {`${user.country.code} 66 | )} 67 |
68 | 69 | {user.email}{' '} 70 | 71 | 72 | {user.roles?.[0].toUpperCase()} 73 | 74 |
75 | 76 | 85 | 86 |
87 | ); 88 | } 89 | 90 | export const loginCardDefinition: ComponentDefinition = { 91 | id: 'login-card', 92 | name: 'Login Card', 93 | category: 'Components', 94 | thumbnailUrl: 95 | 'https://images.ctfassets.net/yv5x7043a54k/3f0c2nKdZlCXIr1LDz0qAT/298b9b5fd1d5b6f9aa771566f1b4f285/login_card.svg', 96 | tooltip: { 97 | description: 'Component tooltip', 98 | }, 99 | builtInStyles: [ 100 | 'cfBackgroundColor', 101 | 'cfBorder', 102 | 'cfBorderRadius', 103 | 'cfFontSize', 104 | 'cfLetterSpacing', 105 | 'cfLineHeight', 106 | 'cfMargin', 107 | 'cfMaxWidth', 108 | 'cfPadding', 109 | 'cfTextAlign', 110 | 'cfTextColor', 111 | 'cfTextTransform', 112 | 'cfWidth', 113 | ], 114 | variables: { 115 | internalId: { 116 | displayName: 'Internal ID', 117 | type: 'Text', 118 | }, 119 | avatar: { 120 | displayName: 'Avatar', 121 | type: 'Media', 122 | }, 123 | firstName: { 124 | displayName: 'First Name', 125 | type: 'Text', 126 | }, 127 | lastName: { 128 | displayName: 'Last Name', 129 | type: 'Text', 130 | }, 131 | email: { 132 | displayName: 'Email', 133 | type: 'Text', 134 | }, 135 | phone: { 136 | displayName: 'Phone', 137 | type: 'Text', 138 | }, 139 | company: { 140 | displayName: 'Company', 141 | type: 'Link', 142 | }, 143 | role: { 144 | displayName: 'Role', 145 | type: 'Text', 146 | }, 147 | country: { 148 | displayName: 'Country', 149 | type: 'Text', 150 | }, 151 | }, 152 | }; 153 | -------------------------------------------------------------------------------- /src/components/layout/header-slot/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon, SearchBox } from '@/components/designSystem'; 4 | import { useAppContext, useSiteConfig, useSiteLabels } from '@/hooks'; 5 | import { Button, Drawer, Typography } from '@material-tailwind/react'; 6 | import { useRouter } from 'next/navigation'; 7 | import React from 'react'; 8 | import EyebrowNavigation from './eyebrow-navigation'; 9 | import HeaderTools from './header-tools'; 10 | import LocaleSelector from './locale-selector'; 11 | import PrimaryNav from './primary-nav'; 12 | import SignIn from './sign-in'; 13 | import SiteLogo from './site-logo'; 14 | 15 | export default function HeaderSlot() { 16 | const { state, logout } = useAppContext(); 17 | const { currentUser: user } = state; 18 | const { siteConfig } = useSiteConfig(); 19 | const { siteLabels } = useSiteLabels(); 20 | const router = useRouter(); 21 | 22 | const [open, setOpen] = React.useState(false); 23 | const openDrawer = () => setOpen(true); 24 | const closeDrawer = () => setOpen(false); 25 | 26 | const handleLogout = () => { 27 | logout(); 28 | router.push('/'); 29 | }; 30 | 31 | return ( 32 | <> 33 |
34 |
35 |
36 |
37 | 38 |
39 | 43 | {siteConfig.siteName} 44 | 45 |
46 |
47 | 48 | 56 |
57 |
58 |
59 | {user ? : } 60 |
61 |
62 | 63 | {user && ( 64 | <> 65 |
68 |
69 | 80 |
81 |
82 | 83 |
84 |
85 | 86 | {/* */} 87 |
88 |
89 | 90 | 91 | )} 92 | 93 | ); 94 | } 95 | 96 | const NavigationDrawer = (props: any) => { 97 | const { siteLabels } = useSiteLabels(); 98 | const { closeDrawer, open } = props; 99 | return ( 100 | 101 |
102 | 112 |
113 |
114 | 115 |
116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/designSystem/content/icon.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 2 | import { findIconDefinition } from '@fortawesome/fontawesome-svg-core'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import EditText from '../shared/edit-text'; 5 | 6 | import { library } from '@fortawesome/fontawesome-svg-core'; 7 | import { fab } from '@fortawesome/free-brands-svg-icons'; 8 | import { fas } from '@fortawesome/free-solid-svg-icons'; 9 | import { far } from '@fortawesome/free-regular-svg-icons'; 10 | import Link from 'next/link'; 11 | import { useEditMode } from '@/hooks'; 12 | 13 | library.add(fab, far, fas); 14 | 15 | export default function Icon(props: any) { 16 | const { 17 | className, 18 | color, 19 | prefix, 20 | iconName, 21 | link, 22 | animation, 23 | flip, 24 | rotation, 25 | size, 26 | } = props; 27 | 28 | const { editMode } = useEditMode(); 29 | 30 | const icon = findIconDefinition({ 31 | prefix, 32 | iconName, 33 | }); 34 | 35 | const FAIcon = () => { 36 | return ( 37 | 47 | ); 48 | }; 49 | 50 | return ( 51 | <> 52 | {!icon ? ( 53 | editMode && 54 | ) : link ? ( 55 | 56 | 57 | 58 | ) : ( 59 | 60 | )} 61 | 62 | ); 63 | } 64 | 65 | export const iconDefinition: ComponentDefinition = { 66 | id: 'icon', 67 | name: 'Font Awesome Icon', 68 | category: 'Elements', 69 | thumbnailUrl: 70 | 'https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600', 71 | tooltip: { 72 | description: 'Renders a Font Awesome Icon', 73 | }, 74 | builtInStyles: [ 75 | 'cfBackgroundColor', 76 | 'cfBorder', 77 | 'cfBorderRadius', 78 | 'cfFontSize', 79 | 'cfMargin', 80 | 'cfMaxWidth', 81 | 'cfPadding', 82 | 'cfTextColor', 83 | 'cfWidth', 84 | ], 85 | variables: { 86 | prefix: { 87 | displayName: 'Icon Package', 88 | type: 'Text', 89 | group: 'content', 90 | defaultValue: 'fas', 91 | validations: { 92 | in: [ 93 | { displayName: 'brands', value: 'fab' }, 94 | { displayName: 'regular', value: 'far' }, 95 | { displayName: 'solid', value: 'fas' }, 96 | ], 97 | }, 98 | }, 99 | iconName: { 100 | displayName: 'Icon Name', 101 | type: 'Text', 102 | group: 'content', 103 | }, 104 | link: { 105 | displayName: 'Link', 106 | type: 'Text', 107 | group: 'content', 108 | }, 109 | animation: { 110 | displayName: 'Animation', 111 | type: 'Text', 112 | group: 'style', 113 | defaultValue: '', 114 | validations: { 115 | in: [ 116 | { displayName: 'none', value: '' }, 117 | { displayName: 'pulse', value: 'pulse' }, 118 | { displayName: 'spin', value: 'spin' }, 119 | ], 120 | }, 121 | }, 122 | flip: { 123 | displayName: 'Flip', 124 | type: 'Text', 125 | group: 'style', 126 | defaultValue: '', 127 | validations: { 128 | in: [ 129 | { displayName: 'none', value: '' }, 130 | { displayName: 'horizontal', value: 'horizontal' }, 131 | { displayName: 'vertical', value: 'vertical' }, 132 | { displayName: 'both', value: 'both' }, 133 | ], 134 | }, 135 | }, 136 | rotation: { 137 | displayName: 'Rotation', 138 | type: 'Number', 139 | group: 'style', 140 | defaultValue: 0, 141 | }, 142 | size: { 143 | displayName: 'Size', 144 | type: 'Text', 145 | group: 'style', 146 | defaultValue: '1x', 147 | validations: { 148 | in: [ 149 | { displayName: '2xs', value: '2xs' }, 150 | { displayName: 'xs', value: 'xs' }, 151 | { displayName: 'sm', value: 'sm' }, 152 | { displayName: 'lg', value: 'lg' }, 153 | { displayName: 'xl', value: 'xl' }, 154 | { displayName: '2xl', value: '2xl' }, 155 | { displayName: '1x', value: '1x' }, 156 | { displayName: '2x', value: '2x' }, 157 | { displayName: '3x', value: '3x' }, 158 | { displayName: '4x', value: '4x' }, 159 | { displayName: '5x', value: '5x' }, 160 | { displayName: '6x', value: '6x' }, 161 | { displayName: '7x', value: '7x' }, 162 | { displayName: '8x', value: '8x' }, 163 | { displayName: '9x', value: '9x' }, 164 | { displayName: '10x', value: '10x' }, 165 | ], 166 | }, 167 | }, 168 | }, 169 | }; 170 | -------------------------------------------------------------------------------- /src/components/designSystem/navigation/menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { EditText } from '@/components/designSystem'; 3 | import { useAppContext, useEditMode } from '@/hooks'; 4 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 5 | import { List } from '@material-tailwind/react'; 6 | import { usePathname } from 'next/navigation'; 7 | import React from 'react'; 8 | import MenuItem from './menu-item'; 9 | 10 | export default function Menu(props: any): JSX.Element { 11 | const { editMode } = useEditMode(); 12 | const { 13 | menuitems, 14 | direction = 'horizontal', 15 | menuicons = 'never', 16 | iconsize = 'h-5 w-5', 17 | fontSize = 'text-base', 18 | handleLinkClick = null, 19 | } = props; 20 | const { state } = useAppContext(); 21 | const userroles = state.currentUserRoles; 22 | const pathname = usePathname(); 23 | 24 | const flex = direction === 'horizontal' ? 'flex flex-row' : ''; 25 | 26 | const userAllowed = (blockedRoles: string[]): boolean => { 27 | if (!blockedRoles) return true; 28 | 29 | const chances = userroles.length; 30 | let fails = 0; 31 | 32 | userroles.forEach((role) => { 33 | const fail = blockedRoles.includes(role); 34 | if (fail) fails = fails + 1; 35 | }); 36 | 37 | return fails < chances; 38 | }; 39 | 40 | return ( 41 | <> 42 | {!menuitems && editMode ? ( 43 | 44 | ) : ( 45 | 46 | {menuitems?.map((menuitem: any, key: number) => { 47 | const isActive = pathname === menuitem.href; 48 | return ( 49 | 50 | {userAllowed(menuitem?.disallowedRoles) && ( 51 | 61 | )} 62 | 63 | ); 64 | })} 65 | 66 | )} 67 | 68 | ); 69 | } 70 | 71 | export const menuDefinition: ComponentDefinition = { 72 | id: 'menu', 73 | name: 'menu', 74 | category: 'Components', 75 | thumbnailUrl: 76 | 'https://images.ctfassets.net/yv5x7043a54k/1ASqROOjQh8PRDdb448OlP/3d32426db96db78991045573da2b1ba1/bars-solid.svg', 77 | tooltip: { 78 | description: 'Displays a navigation menu', 79 | }, 80 | builtInStyles: [ 81 | 'cfBackgroundColor', 82 | 'cfBorder', 83 | 'cfBorderRadius', 84 | 'cfFontSize', 85 | 'cfLetterSpacing', 86 | 'cfLineHeight', 87 | 'cfMargin', 88 | 'cfMaxWidth', 89 | 'cfPadding', 90 | 'cfTextAlign', 91 | 'cfTextColor', 92 | 'cfTextTransform', 93 | 'cfWidth', 94 | ], 95 | variables: { 96 | menuitems: { 97 | displayName: 'Menu Items', 98 | type: 'Array', 99 | group: 'content', 100 | }, 101 | direction: { 102 | displayName: 'Direction', 103 | type: 'Text', 104 | group: 'style', 105 | defaultValue: 'horizontal', 106 | validations: { 107 | in: [ 108 | { displayName: 'Horizontal', value: 'horizontal' }, 109 | { displayName: 'Vertical', value: 'vertical' }, 110 | ], 111 | }, 112 | }, 113 | menuicons: { 114 | displayName: 'Display menu icons', 115 | type: 'Text', 116 | group: 'style', 117 | defaultValue: 'never', 118 | validations: { 119 | in: [ 120 | { displayName: 'only', value: 'only' }, 121 | { displayName: 'on left', value: 'left' }, 122 | { displayName: 'on right', value: 'right' }, 123 | { displayName: 'never', value: 'never' }, 124 | ], 125 | }, 126 | }, 127 | iconsize: { 128 | displayName: 'Icons size', 129 | type: 'Text', 130 | group: 'style', 131 | defaultValue: 'h-4 w-4', 132 | validations: { 133 | in: [ 134 | { displayName: 'xs (12x12)', value: 'h-2 w-2' }, 135 | { displayName: 'sm (14x14)', value: 'h-3 w-3' }, 136 | { displayName: 'default', value: 'h-4 w-4' }, 137 | { displayName: 'lg (20x20)', value: 'h-5 w-5' }, 138 | { displayName: 'xl (24x24)', value: 'h-6 w-6' }, 139 | { displayName: '2xl (32x32)', value: 'h-8 w-8' }, 140 | ], 141 | }, 142 | }, 143 | fontSize: { 144 | displayName: 'Font Size', 145 | type: 'Text', 146 | group: 'style', 147 | defaultValue: 'inherit', 148 | validations: { 149 | in: [ 150 | { displayName: 'xs', value: 'text-xs' }, 151 | { displayName: 'sm', value: 'text-sm' }, 152 | { displayName: 'md', value: 'text-base' }, 153 | { displayName: 'lg', value: 'text-lg' }, 154 | { displayName: 'xl', value: 'text-xl' }, 155 | { displayName: '2xl', value: 'text-2xl' }, 156 | ], 157 | }, 158 | }, 159 | }, 160 | }; 161 | -------------------------------------------------------------------------------- /src/components/designSystem/index.tsx: -------------------------------------------------------------------------------- 1 | import MiniCart from './carts/mini-cart'; 2 | import Article from './content/articles/article'; 3 | import ArticleCard from './content/articles/article-card'; 4 | import ArticleList from './content/articles/article-list'; 5 | import Button from './content/button'; 6 | import FAQ from './content/faqs/faq'; 7 | import FAQList from './content/faqs/faq-list'; 8 | import Heading from './content/heading'; 9 | import Hero from './content/hero'; 10 | import Icon from './content/icon'; 11 | import ProductCollection from './content/products/product-collection'; 12 | import ProfileCard from './content/profile-card'; 13 | import PromoCard from './content/promo-card'; 14 | import Rating from './content/rating'; 15 | import Testimonial from './content/testimonial'; 16 | import DataWidget from './content/widgets/data-widget'; 17 | import InfoWidget from './content/widgets/info-widget'; 18 | import DataTable from './data-table/data-table'; 19 | import Breadcrumbs from './navigation/breadcrumbs'; 20 | import Menu from './navigation/menu'; 21 | import MenuItem from './navigation/menu-item'; 22 | import OrderDetailsModal from './orders/order-details-modal'; 23 | import OrderHistory from './orders/order-history'; 24 | import QuickOrderModal from './orders/quick-order-modal'; 25 | import ProductCard from './products/product-card'; 26 | import ProductDetails from './products/product-details'; 27 | import ProductFacets from './products/product-facets'; 28 | import ProductList from './products/product-list'; 29 | import QuoteDetailsModal from './quotes/quote-details-modal'; 30 | import QuoteHistory from './quotes/quote-history'; 31 | import SearchBox from './search/search-box'; 32 | import SearchResults from './search/search-results'; 33 | import ContentError from './shared/content-error'; 34 | import EditText from './shared/edit-text'; 35 | import GridButton from './shared/grid-button'; 36 | import Pagination from './shared/pagination'; 37 | import Sorts from './shared/sorts'; 38 | import TicketDetailsModal from './tickets/ticket-details-modal'; 39 | import TicketHistory from './tickets/ticket-history'; 40 | import LoginCard from './users/login-card'; 41 | import LoginCards from './users/login-cards'; 42 | import LogoutModal from './users/logout-modal'; 43 | import MiniProfile from './users/mini-profile'; 44 | import ProfileDetailsModal from './users/profile-details-modal'; 45 | 46 | export { alertDefinition } from './content/alert'; 47 | export { articleDefinition } from './content/articles/article'; 48 | export { articleCardDefinition } from './content/articles/article-card'; 49 | export { articleListDefinition } from './content/articles/article-list'; 50 | export { avatarDefinition } from './content/avatar'; 51 | export { buttonDefinition } from './content/button'; 52 | export { faqDefinition } from './content/faqs/faq'; 53 | export { faqListDefinition } from './content/faqs/faq-list'; 54 | export { headingDefinition } from './content/heading'; 55 | export { heroDefinition } from './content/hero'; 56 | export { productCollectionDefinition } from './content/products/product-collection'; 57 | export { profileCardDefinition } from './content/profile-card'; 58 | export { promoCardDefinition } from './content/promo-card'; 59 | export { ratingDefinition } from './content/rating'; 60 | export { testimonialDefinition } from './content/testimonial'; 61 | export { dataWidgetDefinition } from './content/widgets/data-widget'; 62 | export { infoWidgetDefinition } from './content/widgets/info-widget'; 63 | export { dataTableDefinition } from './data-table/data-table'; 64 | export { breadcrumbsDefinition } from './navigation/breadcrumbs'; 65 | export { menuDefinition } from './navigation/menu'; 66 | export { orderHistoryDefinition } from './orders/order-history'; 67 | export { 68 | CSSColors, 69 | HeadingFormats, 70 | Icons, 71 | TailwindColors, 72 | TextFormats, 73 | } from './picker-options'; 74 | export { productDetailsDefinition } from './products/product-details'; 75 | export { quoteHistoryDefinition } from './quotes/quote-history'; 76 | export { searchResultsDefinition } from './search/search-results'; 77 | export { ticketHistoryDefinition } from './tickets/ticket-history'; 78 | export { loginCardDefinition } from './users/login-card'; 79 | export { loginCardsDefinition } from './users/login-cards'; 80 | export { 81 | Article, 82 | ArticleCard, 83 | ArticleList, 84 | Breadcrumbs, 85 | Button, 86 | ContentError, 87 | DataTable, 88 | DataWidget, 89 | EditText, 90 | FAQ, 91 | FAQList, 92 | GridButton, 93 | Heading, 94 | Hero, 95 | Icon, 96 | InfoWidget, 97 | LoginCard, 98 | LoginCards, 99 | LogoutModal, 100 | Menu, 101 | MenuItem, 102 | MiniCart, 103 | MiniProfile, 104 | OrderDetailsModal, 105 | OrderHistory, 106 | Pagination, 107 | ProductCard, 108 | ProductCollection, 109 | ProductDetails, 110 | ProductFacets, 111 | ProductList, 112 | ProfileCard, 113 | ProfileDetailsModal, 114 | PromoCard, 115 | QuickOrderModal, 116 | QuoteDetailsModal, 117 | QuoteHistory, 118 | Rating, 119 | SearchBox, 120 | SearchResults, 121 | Sorts, 122 | Testimonial, 123 | TicketDetailsModal, 124 | TicketHistory, 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/designSystem/orders/order-details-entries.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { OrderEntry } from '@/models/commerce-types'; 3 | import { getSAPProductImageUrl } from '@/utils/image-utils'; 4 | import { localizeCurrency } from '@/utils/locale-utils'; 5 | import { Typography } from '@material-tailwind/react'; 6 | import Image from 'next/image'; 7 | 8 | export const OrderDetailsEntries = (props: any) => { 9 | const { details } = props; 10 | const { status } = details; 11 | 12 | return ( 13 |
14 | 15 |
16 | {details.entries.map((entry: OrderEntry) => { 17 | return ( 18 | 19 | ); 20 | })} 21 |
22 |
23 | ); 24 | }; 25 | 26 | const OrderDetailsEntriesLabels = () => { 27 | const { siteLabels } = useSiteLabels(); 28 | 29 | return ( 30 |
31 | 35 | {siteLabels['label.description']?.toUpperCase()} 36 | 37 | 41 | {siteLabels['label.quantity']?.toUpperCase()} 42 | 43 | 47 | {siteLabels['label.total']?.toUpperCase()} 48 | 49 | 53 | {siteLabels['label.actions']?.toUpperCase()} 54 | 55 |
56 | ); 57 | }; 58 | 59 | const OrderDetailsEntry = (props: any) => { 60 | const { entry, status } = props; 61 | const { state } = useAppContext(); 62 | const { siteLabels } = useSiteLabels(); 63 | 64 | return ( 65 |
66 |
67 |
68 | {entry.product.name} 76 |
77 |
78 |
82 | 87 | {entry.product.code} 88 | 89 | 94 | {localizeCurrency(state.currentLocale, entry.basePrice.value)} 95 | 96 |
97 |
98 | 103 | {entry.quantity} 104 | 105 | 110 | {localizeCurrency(state.currentLocale, entry.totalPrice.value)} 111 | 112 |
113 | {status === 'cancelled' && ( 114 | 121 | {siteLabels['label.reorderItems']} 122 | 123 | )} 124 | {status === 'delivered' && ( 125 | 132 | {siteLabels['label.returnItems']} 133 | 134 | )} 135 | {status === 'received' && ( 136 | 143 | {siteLabels['label.cancelItems']} 144 | 145 | )} 146 |
147 |
148 | ); 149 | }; 150 | -------------------------------------------------------------------------------- /src/components/designSystem/quotes/quote-details-entries.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext, useSiteLabels } from '@/hooks'; 2 | import { OrderEntry } from '@/models/commerce-types'; 3 | import { getSAPProductImageUrl } from '@/utils/image-utils'; 4 | import { localizeCurrency } from '@/utils/locale-utils'; 5 | import { Typography } from '@material-tailwind/react'; 6 | import Image from 'next/image'; 7 | 8 | export const QuoteDetailsEntries = (props: any) => { 9 | const { details } = props; 10 | const { status } = details; 11 | 12 | return ( 13 |
14 | 15 |
16 | {details.entries.map((entry: OrderEntry) => { 17 | return ( 18 | 19 | ); 20 | })} 21 |
22 |
23 | ); 24 | }; 25 | 26 | const QuoteDetailsEntriesLabels = () => { 27 | const { siteLabels } = useSiteLabels(); 28 | 29 | return ( 30 |
31 | 35 | {siteLabels['label.description']?.toUpperCase()} 36 | 37 | 41 | {siteLabels['label.quantity']?.toUpperCase()} 42 | 43 | 47 | {siteLabels['label.total']?.toUpperCase()} 48 | 49 | 53 | {siteLabels['label.actions']?.toUpperCase()} 54 | 55 |
56 | ); 57 | }; 58 | 59 | const QuoteDetailsEntry = (props: any) => { 60 | const { entry, status } = props; 61 | const { state } = useAppContext(); 62 | const { siteLabels } = useSiteLabels(); 63 | 64 | return ( 65 |
66 |
67 |
68 | {entry.product.name} 76 |
77 |
78 |
82 | 87 | {entry.product.code} 88 | 89 | 94 | {localizeCurrency(state.currentLocale, entry.basePrice.value)} 95 | 96 |
97 |
98 | 103 | {entry.quantity} 104 | 105 | 110 | {localizeCurrency(state.currentLocale, entry.totalPrice.value)} 111 | 112 |
113 | {status === 'cancelled' && ( 114 | 121 | {siteLabels['label.reorderItems']} 122 | 123 | )} 124 | {status === 'delivered' && ( 125 | 132 | {siteLabels['label.returnItems']} 133 | 134 | )} 135 | {status === 'received' && ( 136 | 143 | {siteLabels['label.cancelItems']} 144 | 145 | )} 146 |
147 |
148 | ); 149 | }; 150 | -------------------------------------------------------------------------------- /src/components/designSystem/content/promo-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import { EditText } from '@/components/designSystem'; 3 | import { useEditMode } from '@/hooks'; 4 | import { getContentfulImageUrl } from '@/utils/image-utils'; 5 | import { ComponentDefinition } from '@contentful/experiences-sdk-react'; 6 | import { documentToReactComponents } from '@contentful/rich-text-react-renderer'; 7 | import { 8 | Button, 9 | Card, 10 | CardBody, 11 | CardFooter, 12 | CardHeader, 13 | Typography, 14 | } from '@material-tailwind/react'; 15 | import Image from 'next/image'; 16 | import Link from 'next/link'; 17 | import React from 'react'; 18 | import Icon from './icon'; 19 | 20 | export default function PromoCard(props: any) { 21 | const { editMode } = useEditMode(); 22 | 23 | const { 24 | title, 25 | summary, 26 | description, 27 | ctaText, 28 | ctaURL, 29 | image, 30 | openInNewWindow, 31 | } = props; 32 | 33 | const isDefined = () => { 34 | return ( 35 | typeof title === 'string' || 36 | typeof summary === 'string' || 37 | typeof description === 'object' || 38 | typeof ctaText === 'string' || 39 | typeof ctaURL === 'string' || 40 | typeof image === 'string' 41 | ); 42 | }; 43 | 44 | return ( 45 | <> 46 | {!isDefined() ? ( 47 | editMode && 48 | ) : ctaURL ? ( 49 | 53 | 54 | 55 | ) : ( 56 | 57 | )} 58 | 59 | ); 60 | } 61 | 62 | export const promoCardDefinition: ComponentDefinition = { 63 | id: 'promo-card', 64 | name: 'Promo Card', 65 | category: 'Components', 66 | thumbnailUrl: 67 | 'https://images.ctfassets.net/yv5x7043a54k/6VeodakTF7O1tFyUe0wYx/68c6a00726e76ff4786caa56b5a8fed2/promo_card.svg', 68 | tooltip: { 69 | description: 'Card for displaying a promotion', 70 | }, 71 | builtInStyles: [ 72 | 'cfBackgroundColor', 73 | 'cfBorder', 74 | 'cfBorderRadius', 75 | 'cfFontSize', 76 | 'cfLetterSpacing', 77 | 'cfLineHeight', 78 | 'cfMargin', 79 | 'cfMaxWidth', 80 | 'cfPadding', 81 | 'cfTextAlign', 82 | 'cfTextColor', 83 | 'cfTextTransform', 84 | 'cfWidth', 85 | ], 86 | variables: { 87 | title: { 88 | displayName: 'Title', 89 | type: 'Text', 90 | }, 91 | summary: { 92 | displayName: 'Summary', 93 | type: 'Text', 94 | }, 95 | description: { 96 | displayName: 'Description', 97 | type: 'RichText', 98 | }, 99 | ctaText: { 100 | displayName: 'CTA Text', 101 | type: 'Text', 102 | }, 103 | ctaURL: { 104 | displayName: 'CTA URL', 105 | type: 'Text', 106 | }, 107 | openInNewWindow: { 108 | displayName: 'OpenInNewWindow', 109 | type: 'Boolean', 110 | group: 'content', 111 | defaultValue: false, 112 | }, 113 | image: { 114 | displayName: 'Image', 115 | type: 'Media', 116 | }, 117 | }, 118 | }; 119 | 120 | const PromoCardCard = (props: any): React.JSX.Element => { 121 | const { 122 | title, 123 | summary, 124 | description, 125 | ctaText, 126 | ctaURL, 127 | image, 128 | border, 129 | shadow, 130 | } = props; 131 | 132 | return ( 133 | 137 | {image && ( 138 | 144 | {image && ( 145 | {title} 153 | )} 154 | 155 | )} 156 | 157 | {title && ( 158 | 159 | {title} 160 | 161 | )} 162 | {summary && ( 163 | 169 | {summary} 170 | 171 | )} 172 | {description && ( 173 | 179 | {documentToReactComponents(description)} 180 | 181 | )} 182 | 183 | {ctaText && ctaURL && ( 184 | 185 |
186 | {ctaText && ( 187 | 191 | )} 192 |
193 |
194 | )} 195 |
196 | ); 197 | }; 198 | -------------------------------------------------------------------------------- /src/components/designSystem/users/mini-profile.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Icon } from '@/components/designSystem'; 4 | import { useSiteLabels } from '@/hooks'; 5 | import useAppContext from '@/hooks/app-context'; 6 | import { getUser } from '@/mocks'; 7 | import { User } from '@/models/commerce-types'; 8 | import { 9 | Avatar, 10 | Button, 11 | Chip, 12 | Menu, 13 | MenuHandler, 14 | MenuItem, 15 | MenuList, 16 | Typography, 17 | } from '@material-tailwind/react'; 18 | import { useRouter } from 'next/navigation'; 19 | import React from 'react'; 20 | import ProfileDetailsModal from './profile-details-modal'; 21 | 22 | export default function MiniProfile() { 23 | const { state, logout } = useAppContext(); 24 | const { currentUser: guid, currentLocale: locale } = state; 25 | const { siteLabels } = useSiteLabels(); 26 | 27 | const router = useRouter(); 28 | 29 | const [error, setError] = React.useState(); 30 | const [user, setUser] = React.useState(); 31 | const [open, setOpen] = React.useState(false); 32 | const [showProfile, setShowProfile] = React.useState(false); 33 | // const [showLogoutModal, setShowLogoutModal] = React.useState(); 34 | 35 | React.useEffect(() => { 36 | let isMounted = true; 37 | if (!guid) return; 38 | 39 | const loadUser = async () => { 40 | await getUser(guid).then((newUser) => { 41 | if (newUser) { 42 | if (isMounted) { 43 | setUser(newUser); 44 | } 45 | } 46 | }); 47 | }; 48 | 49 | loadUser(); 50 | 51 | return () => { 52 | isMounted = false; 53 | }; 54 | }, [state, guid]); 55 | 56 | const handleLogout = () => { 57 | // setShowLogoutModal(true); 58 | logout(); 59 | router.push('/'); 60 | }; 61 | 62 | const handleCloseProfile = () => { 63 | setShowProfile(false); 64 | }; 65 | 66 | const handleEditProfile = () => { 67 | setShowProfile(false); 68 | }; 69 | 70 | const handleOpenProfile = () => { 71 | setShowProfile(true); 72 | // router.push('/profile'); 73 | }; 74 | 75 | const handleSupportLink = () => { 76 | router.push('/support'); 77 | }; 78 | 79 | const toggleOpen = () => { 80 | setOpen((open) => { 81 | return !open; 82 | }); 83 | }; 84 | 85 | return ( 86 | <> 87 | {error &&
{error}
} 88 | {user && ( 89 | <> 90 |
91 | 97 | 98 | 114 | 115 | 116 | 120 | 121 | {siteLabels['label.profile']} 122 | 123 | 124 | 125 | {siteLabels['label.editProfile']} 126 | 127 | 128 | 129 | {siteLabels['label.inbox']} 130 | {user.roles.includes('approver') && ( 131 | 138 | )} 139 | 140 | 144 | 145 | {siteLabels['label.support']} 146 | 147 | 151 | 152 | {siteLabels['label.signout']} 153 | 154 | 155 | 156 |
157 | 167 | 168 | )} 169 | {/* */} 170 | 171 | ); 172 | } 173 | --------------------------------------------------------------------------------