├── .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 |
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 |
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 |
35 |
36 | {children}
37 |
38 |
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 |
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 |
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 |
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 |
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 |
78 |
79 | )}
80 | {menuicons === 'only' ? (
81 |
89 | ) : (
90 | text
91 | )}
92 | {menuicons === 'right' && (
93 |
94 |
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 |
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 |
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 |
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 |
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 |
50 | )}
51 |
52 |
53 |
54 |
55 | {user.firstName} {user.lastName}
56 |
57 | {user.country.code && (
58 |
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 |
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 |
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 |
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 |
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 |
156 |
157 |
167 | >
168 | )}
169 | {/* */}
170 | >
171 | );
172 | }
173 |
--------------------------------------------------------------------------------