diff --git a/DOCS/repository_context.txt b/DOCS/repository_context.txt deleted file mode 100644 index a1e07a15d..000000000 --- a/DOCS/repository_context.txt +++ /dev/null @@ -1,5893 +0,0 @@ -This file is a merged representation of the entire codebase, combining all repository files into a single document. -Generated by Repomix on: 2025-01-30T04:52:32.241Z - -# File Summary - -## Purpose -This file contains a packed representation of the entire repository's contents. -It is designed to be easily consumable by AI systems for analysis, code review, -or other automated processes. - -## File Format -The content is organized as follows: -1. This summary section -2. Repository information -3. Directory structure -4. Multiple file entries, each consisting of: - a. A header with the file path (## File: path/to/file) - b. The full contents of the file in a code block - -## Usage Guidelines -- This file should be treated as read-only. Any changes should be made to the - original repository files, not this packed version. -- When processing this file, use the file path to distinguish - between different files in the repository. -- Be aware that this file may contain sensitive information. Handle it with - the same level of security as you would the original repository. - -## Notes -- Some files may have been excluded based on .gitignore rules and Repomix's - configuration. -- Binary files are not included in this packed representation. Please refer to - the Repository Structure section for a complete list of file paths, including - binary files. - -## Additional Info - -# Directory Structure -``` -.github/ - ISSUE_TEMPLATE/ - bug_report.md - workflows/ - codeql.yml - playwright.yml - repomix.yml -public/ - favicon.svg -src/ - components/ - AlgoliaSearch/ - AlgoliaSearchBox.component.tsx - MobileSearch.component.tsx - SearchResults.component.tsx - Animations/ - types/ - Animations.types.ts - FadeLeftToRight.component.tsx - FadeLeftToRightItem.component.tsx - FadeUp.component.tsx - Cart/ - CartContents.component.tsx - Category/ - Categories.component.tsx - Checkout/ - Billing.component.tsx - CheckoutForm.component.tsx - Footer/ - Footer.component.tsx - Hamburger.component.tsx - Stickynav.component.tsx - Header/ - Cart.component.tsx - Header.component.tsx - Navbar.component.tsx - Index/ - Hero.component.tsx - Input/ - InputField.component.tsx - Layout/ - Layout.component.tsx - PageTitle.component.tsx - LoadingSpinner/ - LoadingSpinner.component.tsx - Product/ - AddToCart.component.tsx - DisplayProducts.component.tsx - ProductCard.component.tsx - ProductFilters.component.tsx - ProductList.component.tsx - SingleProduct.component.tsx - SVG/ - SVGMobileSearchIcon.component.tsx - UI/ - Button.component.tsx - Checkbox.component.tsx - RangeSlider.component.tsx - User/ - UserRegistration.component.tsx - hooks/ - useProductFilters.ts - pages/ - kategori/ - [slug].tsx - produkt/ - [slug].tsx - _app.tsx - _document.tsx - handlekurv.tsx - index.tsx - kasse.tsx - kategorier.tsx - produkter.tsx - stores/ - CartProvider.tsx - styles/ - algolia.min.css - animate.min.css - globals.css - tests/ - Index/ - Index.spec.ts - Produkter/ - Produkter.spec.ts - example.spec.txt - types/ - product.ts - utils/ - apollo/ - ApolloClient.js - constants/ - INPUT_FIELDS.ts - LINKS.ts - functions/ - functions.tsx - productUtils.ts - gql/ - GQL_MUTATIONS.ts - GQL_QUERIES.ts -.codeclimate.yml -.env.example -.eslintrc.json -.gitignore -.prettierrc -CODE_OF_CONDUCT.md -CONTRIBUTING.md -LICENSE -next.config.js -package.json -playwright.config.ts -postcss.config.js -README.md -renovate.json -SUGGESTIONS.md -tailwind.config.js -tsconfig.json -``` - -# Files - -## File: .github/ISSUE_TEMPLATE/bug_report.md -```markdown -* * * - -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - -* * * - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - -- OS: [e.g. iOS] -- Browser [e.g. chrome, safari] -- Version [e.g. 22] - -**Smartphone (please complete the following information):** - -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, safari] -- Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. -``` - -## File: .github/workflows/codeql.yml -```yaml -name: "CodeQL" -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: "5 12 * * 2" -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - strategy: - fail-fast: false - matrix: - language: [ javascript ] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{ matrix.language }}" -``` - -## File: .github/workflows/playwright.yml -```yaml -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Build the project - run: npm run build - env: - NEXT_PUBLIC_GRAPHQL_URL: ${{ secrets.NEXT_PUBLIC_GRAPHQL_URL }} - - name: Start the application - run: npm run start & - env: - NEXT_PUBLIC_GRAPHQL_URL: ${{ secrets.NEXT_PUBLIC_GRAPHQL_URL }} - - name: Wait for the application to be ready - run: | - echo "Waiting for the application to be ready..." - timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:3000)" != "200" ]]; do sleep 5; done' || false - echo "Application is ready!" - - name: Run Playwright tests - run: npx playwright test - env: - CI: true - NEXT_PUBLIC_GRAPHQL_URL: ${{ secrets.NEXT_PUBLIC_GRAPHQL_URL }} - DEBUG: pw:api - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 - - name: Upload test traces - if: failure() - uses: actions/upload-artifact@v4 - with: - name: playwright-traces - path: test-results/ - retention-days: 30 -``` - -## File: .github/workflows/repomix.yml -```yaml -name: Repository Documentation -on: - push: - branches: - - main - workflow_dispatch: # allows manual triggering -permissions: - contents: write - pull-requests: write -jobs: - analyze: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # fetch all history for better context - - name: Wait for other checks - run: | - echo "Waiting for 5 minutes to allow other checks to complete..." - sleep 300 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - name: Install Repomix - run: npm install -g repomix - - name: Generate Repository Documentation - run: | - echo "Creating DOCS directory..." - mkdir -p DOCS - echo "Running Repomix..." - if ! repomix --output DOCS/repository_context.txt --style markdown --remove-empty-lines --verbose; then - echo "Error: Repomix command failed" - # Print directory contents for debugging - echo "DOCS directory contents:" - ls -la DOCS/ - exit 1 - fi - echo "Verifying output file..." - if [ ! -f "DOCS/repository_context.txt" ]; then - echo "Error: repository_context.txt was not created" - # Print directory contents for debugging - echo "DOCS directory contents:" - ls -la DOCS/ - exit 1 - fi - if [ ! -s "DOCS/repository_context.txt" ]; then - echo "Error: repository_context.txt is empty" - exit 1 - fi - echo "Repository context file generated successfully" - echo "File size: $(stat --format=%s "DOCS/repository_context.txt") bytes" - echo "First few lines of the file:" - head -n 5 "DOCS/repository_context.txt" - # Update Documentation - - name: Commit and Push Changes - run: | - echo "Configuring git..." - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - echo "Checking for changes..." - if [[ -n "$(git status --porcelain)" ]]; then - echo "Changes detected, committing..." - # Stage only repository_context.txt to avoid unintended changes - if ! git add DOCS/repository_context.txt; then - echo "Error: Failed to stage repository_context.txt" - exit 1 - fi - if ! git commit -m "docs: update repository context via Repomix [skip ci]"; then - echo "Error: Failed to create commit" - exit 1 - fi - echo "Pushing to main branch..." - if ! git push; then - echo "Error: Failed to push changes" - exit 1 - fi - echo "Successfully updated repository context" - else - echo "No changes detected in repository_context.txt" - fi -``` - -## File: public/favicon.svg -``` - -``` - -## File: src/components/AlgoliaSearch/AlgoliaSearchBox.component.tsx -```typescript -import algoliasearch from 'algoliasearch'; -import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-dom'; -import { useState } from 'react'; -import SearchResults from './SearchResults.component'; -const searchClient = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID ?? 'changeme', - process.env.NEXT_PUBLIC_ALGOLIA_PUBLIC_API_KEY ?? 'changeme', -); -// https://www.algolia.com/doc/api-reference/widgets/instantsearch/react/ -/** - * Displays Algolia search for larger resolutions that do not show the mobile menu - */ -const AlgoliaSearchBox = () => { - const [search, setSearch] = useState(null); - const [hasFocus, sethasFocus] = useState(false); - return ( -
-
- - {/*We need to conditionally add a border because the element has position:fixed*/} - { - const target = event.target as HTMLInputElement; - sethasFocus(true); - setSearch(target.value); - }} - onKeyDown={(event) => { - const target = event.target as HTMLInputElement; - sethasFocus(true); - setSearch(target.value); - }} - onReset={() => { - setSearch(null); - }} - /> - {search && ( -
- -
- )} -
-
-
- ); -}; -export default AlgoliaSearchBox; -``` - -## File: src/components/AlgoliaSearch/MobileSearch.component.tsx -```typescript -import algoliasearch from 'algoliasearch'; -import { InstantSearch, SearchBox, Hits } from 'react-instantsearch-dom'; -import { useState } from 'react'; -import SearchResults from './SearchResults.component'; -const searchClient = algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID ?? 'changethis', - process.env.NEXT_PUBLIC_ALGOLIA_PUBLIC_API_KEY ?? 'changethis', -); -/** - * Algolia search for mobile menu. - */ -const MobileSearch = () => { - const [search, setSearch] = useState(null); - const [hasFocus, sethasFocus] = useState(false); - return ( -
- - { - setSearch(null); - }} - onChange={(event) => { - const target = event.target as HTMLInputElement; - sethasFocus(true); - setSearch(target.value); - }} - onKeyDown={(event) => { - const target = event.target as HTMLInputElement; - sethasFocus(true); - setSearch(target.value); - }} - /> - {search && } - -
- ); -}; -export default MobileSearch; -``` - -## File: src/components/AlgoliaSearch/SearchResults.component.tsx -```typescript -import Link from 'next/link'; -import { trimmedStringToLength } from '@/utils/functions/functions'; -interface ISearchResultProps { - hit: { - product_image: string; - product_name: string; - regular_price: string; - sale_price: string; - on_sale: boolean; - short_description: string; - objectID: number; - }; -} -/** - * Displays search results from Algolia - * @param {object} hit { - * @param {string} product_image Product image from WooCommerce - * @param {string} product_name Name of product - * @param {string} regular_price Price without discount - * @param {string} sale_price Price when on sale - * @param {boolean} on_sale Is the product on sale? True or false - * @param {string} short_description Short description of product - * @param {number} objectID ID of product - } - */ -const SearchResults = ({ - hit: { - product_image, - product_name, - regular_price, - sale_price, - on_sale, - short_description, - objectID, - }, -}: ISearchResultProps) => { - // Replace empty spaces with dash (-) - const trimmedProductName = product_name.replace(/ /g, '-'); - return ( -
- -
-
- {product_name} -
-
- {product_name && ( - {product_name} - )} -
- {on_sale && ( - <> - - kr {regular_price} - - kr {sale_price} - - )} - {!on_sale && kr {regular_price}} -
- - {trimmedStringToLength(short_description, 30)} - -
-
- -
- ); -}; -export default SearchResults; -``` - -## File: src/components/Animations/types/Animations.types.ts -```typescript -import { ReactNode } from 'react'; -export interface IAnimateProps { - children: ReactNode; - cssClass?: string; -} -export interface IAnimateBounceProps { - children: ReactNode; - cssClass?: string; - viewAmount?: 'some' | 'all' | number; -} -export interface IAnimateWithDelayProps { - children: ReactNode; - cssClass?: string; - delay: number; -} -export interface IAnimateStaggerWithDelayProps { - children: ReactNode; - cssClass?: string; - delay: number; - staggerDelay?: number; - animateNotReverse: boolean; -} -``` - -## File: src/components/Animations/FadeLeftToRight.component.tsx -```typescript -// CircleCI doesn't like import { motion } from "framer-motion" here, so we use require -const { motion } = require('framer-motion'); -import type { IAnimateStaggerWithDelayProps } from './types/Animations.types'; -/** - * Fade content left to right. Needs to be used with FadeLeftToRightItem - * @function FadeLeftToRight - * @param {ReactNode} children - Children content to render - * @param {string} cssClass - CSS classes to apply to component - * @param {number} delay - Time to wait before starting animation - * @param {number} staggerDelay - Time to wait before starting animation for children items - * @param {boolean} animateNotReverse - Start animation backwards - * @returns {JSX.Element} - Rendered component - */ -const FadeLeftToRight = ({ - children, - cssClass, - delay, - staggerDelay, - animateNotReverse, -}: IAnimateStaggerWithDelayProps) => { - const FadeLeftToRightVariants = { - visible: { - opacity: 1, - transition: { - when: 'beforeChildren', - staggerChildren: staggerDelay ? staggerDelay : 0.5, - delay, - ease: 'easeInOut', - staggerDirection: 1, - }, - }, - hidden: { - opacity: 0, - transition: { - when: 'afterChildren', - staggerChildren: staggerDelay ? staggerDelay : 0.5, - staggerDirection: -1, - }, - }, - }; - return ( - - {children} - - ); -}; -export default FadeLeftToRight; -``` - -## File: src/components/Animations/FadeLeftToRightItem.component.tsx -```typescript -// CircleCI doesn't like import { motion } from "framer-motion" here, so we use require -const { motion } = require('framer-motion'); -import type { IAnimateProps } from './types/Animations.types'; -/** - * Fade content left to right. Needs to be used with FadeLeftToRight as parent container - * @function FadeLeftToRightItem - * @param {ReactNode} children - Children content to render - * @param {string} cssClass - CSS classes to apply to component - * @returns {JSX.Element} - Rendered component - */ -const FadeLeftToRightItem = ({ children, cssClass }: IAnimateProps) => { - const FadeLeftToRightItemVariants = { - visible: { opacity: 1, x: 0 }, - hidden: { opacity: 0, x: -20 }, - }; - return ( - - {children} - - ); -}; -export default FadeLeftToRightItem; -``` - -## File: src/components/Animations/FadeUp.component.tsx -```typescript -// CircleCI doesn't like import { motion } from "framer-motion" here, so we use require -const { motion } = require('framer-motion'); -import type { IAnimateWithDelayProps } from './types/Animations.types'; -/** - * Fade up content animation - * @function FadeUp - * @param {ReactNode} children - Children content to render - * @param {string} cssClass - CSS classes to apply to component - * @param {number} delay - Time to wait before starting animation - * @returns {JSX.Element} - Rendered component - */ -const FadeUp = ({ children, cssClass, delay }: IAnimateWithDelayProps) => { - const fadeUpVariants = { - initial: { opacity: 0, y: 20 }, - animate: { - y: 0, - opacity: 1, - transition: { delay, type: 'spring', duration: 0.5, stiffness: 110 }, - }, - }; - return ( - - {children} - - ); -}; -export default FadeUp; -``` - -## File: src/components/Cart/CartContents.component.tsx -```typescript -import { useContext, useEffect } from 'react'; -import { useMutation, useQuery } from '@apollo/client'; -import Link from 'next/link'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import { v4 as uuidv4 } from 'uuid'; -import { CartContext } from '@/stores/CartProvider'; -import Button from '@/components/UI/Button.component'; -import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component'; -import { - getFormattedCart, - getUpdatedItems, - handleQuantityChange, - IProductRootObject, -} from '@/utils/functions/functions'; -import { GET_CART } from '@/utils/gql/GQL_QUERIES'; -import { UPDATE_CART } from '@/utils/gql/GQL_MUTATIONS'; -const CartContents = () => { - const router = useRouter(); - const { setCart } = useContext(CartContext); - const isCheckoutPage = router.pathname === '/kasse'; - const { data, refetch } = useQuery(GET_CART, { - notifyOnNetworkStatusChange: true, - onCompleted: () => { - const updatedCart = getFormattedCart(data); - if (!updatedCart && !data.cart.contents.nodes.length) { - localStorage.removeItem('woocommerce-cart'); - setCart(null); - return; - } - localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart)); - setCart(updatedCart); - }, - }); - const [updateCart, { loading: updateCartProcessing }] = useMutation( - UPDATE_CART, - { - onCompleted: () => { - refetch(); - setTimeout(() => { - refetch(); - }, 3000); - }, - }, - ); - const handleRemoveProductClick = ( - cartKey: string, - products: IProductRootObject[], - ) => { - if (products?.length) { - const updatedItems = getUpdatedItems(products, 0, cartKey); - updateCart({ - variables: { - input: { - clientMutationId: uuidv4(), - items: updatedItems, - }, - }, - }); - } - refetch(); - setTimeout(() => { - refetch(); - }, 3000); - }; - useEffect(() => { - refetch(); - }, [refetch]); - const cartTotal = data?.cart?.total || '0'; - const getUnitPrice = (subtotal: string, quantity: number) => { - const numericSubtotal = parseFloat(subtotal.replace(/[^0-9.-]+/g, '')); - return isNaN(numericSubtotal) - ? 'N/A' - : (numericSubtotal / quantity).toFixed(2); - }; - return ( -
- {data?.cart?.contents?.nodes?.length ? ( - <> -
- {data.cart.contents.nodes.map((item: IProductRootObject) => ( -
-
- {item.product.node.name} -
-
-

- {item.product.node.name} -

-

- kr {getUnitPrice(item.subtotal, item.quantity)} -

-
-
- { - handleQuantityChange( - event, - item.key, - data.cart.contents.nodes, - updateCart, - updateCartProcessing, - ); - }} - className="w-16 px-2 py-1 text-center border border-gray-300 rounded mr-2" - /> - -
-
-

{item.subtotal}

-
-
- ))} -
-
-
- Subtotal: - {cartTotal} -
- {!isCheckoutPage && ( -
- - - -
- )} -
- - ) : ( -
-

- Ingen produkter i handlekurven -

- - - -
- )} - {updateCartProcessing && ( -
-
-

Oppdaterer handlekurv...

- -
-
- )} -
- ); -}; -export default CartContents; -``` - -## File: src/components/Category/Categories.component.tsx -```typescript -import Link from 'next/link'; -import { v4 as uuidv4 } from 'uuid'; -interface ICategoriesProps { - categories: { id: string; name: string; slug: string }[]; -} -const Categories = ({ categories }: ICategoriesProps) => ( -
-
- {categories.map(({ id, name, slug }) => ( - -
-
-

{name}

-
-
- - ))} -
-
-); -export default Categories; -``` - -## File: src/components/Checkout/Billing.component.tsx -```typescript -// Imports -import { - SubmitHandler, - useForm, - useFormContext, - FormProvider, -} from 'react-hook-form'; -// Components -import { InputField } from '@/components/Input/InputField.component'; -import Button from '../UI/Button.component'; -// Constants -import { INPUT_FIELDS } from '@/utils/constants/INPUT_FIELDS'; -import { ICheckoutDataProps } from '@/utils/functions/functions'; -interface IBillingProps { - handleFormSubmit: SubmitHandler; -} -const OrderButton = () => { - const { register } = useFormContext(); - return ( -
- -
- -
-
- ); -}; -const Billing = ({ handleFormSubmit }: IBillingProps) => { - const methods = useForm(); - return ( -
- -
-
- {INPUT_FIELDS.map(({ id, label, name, customValidation }) => ( - - ))} - -
-
-
-
- ); -}; -export default Billing; -``` - -## File: src/components/Checkout/CheckoutForm.component.tsx -```typescript -/*eslint complexity: ["error", 20]*/ -// Imports -import { useState, useContext, useEffect } from 'react'; -import { useQuery, useMutation, ApolloError } from '@apollo/client'; -// Components -import Billing from './Billing.component'; -import CartContents from '../Cart/CartContents.component'; -import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component'; -// GraphQL -import { GET_CART } from '@/utils/gql/GQL_QUERIES'; -import { CHECKOUT_MUTATION } from '@/utils/gql/GQL_MUTATIONS'; -import { CartContext } from '@/stores/CartProvider'; -// Utils -import { - getFormattedCart, - createCheckoutData, - ICheckoutDataProps, -} from '@/utils/functions/functions'; -export interface IBilling { - firstName: string; - lastName: string; - address1: string; - city: string; - postcode: string; - email: string; - phone: string; -} -export interface IShipping { - firstName: string; - lastName: string; - address1: string; - city: string; - postcode: string; - email: string; - phone: string; -} -export interface ICheckoutData { - clientMutationId: string; - billing: IBilling; - shipping: IShipping; - shipToDifferentAddress: boolean; - paymentMethod: string; - isPaid: boolean; - transactionId: string; -} -const CheckoutForm = () => { - const { cart, setCart } = useContext(CartContext); - const [orderData, setOrderData] = useState(null); - const [requestError, setRequestError] = useState(null); - const [orderCompleted, setorderCompleted] = useState(false); - // Get cart data query - const { data, refetch } = useQuery(GET_CART, { - notifyOnNetworkStatusChange: true, - onCompleted: () => { - // Update cart in the localStorage. - const updatedCart = getFormattedCart(data); - if (!updatedCart && !data.cart.contents.nodes.length) { - localStorage.removeItem('woo-session'); - localStorage.removeItem('wooocommerce-cart'); - setCart(null); - return; - } - localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart)); - // Update cart data in React Context. - setCart(updatedCart); - }, - }); - // Checkout GraphQL mutation - const [checkout, { loading: checkoutLoading }] = useMutation( - CHECKOUT_MUTATION, - { - variables: { - input: orderData, - }, - onCompleted: () => { - localStorage.removeItem('woo-session'); - localStorage.removeItem('wooocommerce-cart'); - setorderCompleted(true); - setCart(null); - refetch(); - }, - onError: (error) => { - setRequestError(error); - refetch(); - }, - }, - ); - useEffect(() => { - if (null !== orderData) { - // Perform checkout mutation when the value for orderData changes. - checkout(); - setTimeout(() => { - refetch(); - }, 2000); - } - }, [checkout, orderData, refetch]); - useEffect(() => { - refetch(); - }, [refetch]); - const handleFormSubmit = (submitData: ICheckoutDataProps) => { - const checkOutData = createCheckoutData(submitData); - setOrderData(checkOutData); - setRequestError(null); - }; - return ( - <> - {cart && !orderCompleted ? ( -
- {/* Order*/} - - {/*Payment Details*/} - - {/*Error display*/} - {requestError && ( -
- En feil har oppstått. -
- )} - {/* Checkout Loading*/} - {checkoutLoading && ( -
- Behandler ordre, vennligst vent ... - -
- )} -
- ) : ( - <> - {!cart && !orderCompleted && ( -

- Ingen produkter i handlekurven -

- )} - {orderCompleted && ( -
- Takk for din ordre! -
- )} - - )} - - ); -}; -export default CheckoutForm; -``` - -## File: src/components/Footer/Footer.component.tsx -```typescript -/** - * Renders Footer of the application. - * @function Footer - * @returns {JSX.Element} - Rendered component - */ -const Footer = () => ( -
-
-
-
- © {new Date().getFullYear()} Daniel / w3bdesign -
-
-
-
-); -export default Footer; -``` - -## File: src/components/Footer/Hamburger.component.tsx -```typescript -import { useState, useEffect, useCallback } from 'react'; -import Link from 'next/link'; -import FadeLeftToRight from '@/components/Animations/FadeLeftToRight.component'; -import FadeLeftToRightItem from '@/components/Animations/FadeLeftToRightItem.component'; -import LINKS from '@/utils/constants/LINKS'; -const hamburgerLine = - 'h-1 w-10 my-1 rounded-full bg-white transition ease transform duration-300 not-sr-only'; -const opacityFull = 'opacity-100 group-hover:opacity-100'; -/** - * Hamburger component used in mobile menu. Animates to a X when clicked - * @function Hamburger - * @param {MouseEventHandler} onClick - onClick handler to respond to clicks - * @param {boolean} isExpanded - Should the hamburger animate to a X? - * @returns {JSX.Element} - Rendered component - */ -const Hamburger = () => { - const [isExpanded, setisExpanded] = useState(false); - const [hidden, setHidden] = useState('invisible'); - useEffect(() => { - if (isExpanded) { - setHidden(''); - } else { - setTimeout(() => { - setHidden('invisible'); - }, 1000); - } - }, [isExpanded]); - const handleMobileMenuClick = useCallback(() => { - /** - * Anti-pattern: setisExpanded(!isExpanded) - * Even if your state updates are batched and multiple updates to the enabled/disabled state are made together - * each update will rely on the correct previous state so that you always end up with the result you expect. - */ - setisExpanded((prevExpanded) => !prevExpanded); - }, [setisExpanded]); - return ( -
- - -
-
    - {LINKS.map(({ id, title, href }) => ( - -
  • - - { - setisExpanded((prevExpanded) => !prevExpanded); - }} - onKeyDown={(event) => { - // 'Enter' key or 'Space' key - if (event.key === 'Enter' || event.key === ' ') { - setisExpanded((prevExpanded) => !prevExpanded); - } - }} - tabIndex={0} // Make the span focusable - role="button" // Indicate the span acts as a button - > - {title} - - -
  • -
    - ))} -
-
-
-
- ); -}; -export default Hamburger; -``` - -## File: src/components/Footer/Stickynav.component.tsx -```typescript -import Link from 'next/link'; -import Cart from '@/components/Header/Cart.component'; -import Search from '@/components/AlgoliaSearch/AlgoliaSearchBox.component'; -import SVGMobileSearchIcon from '@/components/SVG/SVGMobileSearchIcon.component'; -import Hamburger from './Hamburger.component'; -/** - * Navigation for the application. - * Includes mobile menu. - */ -const Stickynav = () => ( - -); -export default Stickynav; -``` - -## File: src/components/Header/Cart.component.tsx -```typescript -import { useContext, useState, useEffect } from 'react'; -import Link from 'next/link'; -import { CartContext } from '@/stores/CartProvider'; -interface ICartProps { - stickyNav?: boolean; -} -/** - * Displays the shopping cart contents. - * Displays amount of items in cart. - */ -const Cart = ({ stickyNav }: ICartProps) => { - const { cart } = useContext(CartContext); - const [productCount, setProductCount] = useState(); - useEffect(() => { - if (cart) { - setProductCount(cart.totalProductsCount); - } else { - setProductCount(null); - } - }, [cart]); - return ( - <> - - - - - - - - - - {productCount && ( - - {productCount} - - )} - - ); -}; -export default Cart; -``` - -## File: src/components/Header/Header.component.tsx -```typescript -import Head from 'next/head'; -import Navbar from './Navbar.component'; -interface IHeaderProps { - title: string; -} -/** - * Renders header for each page. - * @function Header - * @param {string} title - Title for the page. Is set in {title} - * @returns {JSX.Element} - Rendered component - */ -const Header = ({ title }: IHeaderProps) => ( - <> - - {`Next.js webshop with WooCommerce ${title}`} - - - - -
- -
- -); -export default Header; -``` - -## File: src/components/Header/Navbar.component.tsx -```typescript -import Link from 'next/link'; -import Cart from './Cart.component'; -import AlgoliaSearchBox from '../AlgoliaSearch/AlgoliaSearchBox.component'; -import MobileSearch from '../AlgoliaSearch/MobileSearch.component'; -/** - * Navigation for the application. - * Includes mobile menu. - */ -const Navbar = () => { - return ( -
- -
- ); -}; -export default Navbar; -``` - -## File: src/components/Index/Hero.component.tsx -```typescript -import Image from 'next/image'; -import Button from '../UI/Button.component'; -/** - * Renders Hero section for Index page - * @function Hero - * @returns {JSX.Element} - Rendered component - */ -const Hero = () => ( -
-
- Hero image -
-
-
-
-

- Stripete Zig Zag Pute Sett -

- -
-
-
-); -export default Hero; -``` - -## File: src/components/Input/InputField.component.tsx -```typescript -import { FieldValues, useFormContext, UseFormRegister } from 'react-hook-form'; -interface ICustomValidation { - required?: boolean; - minlength?: number; -} -interface IErrors {} -export interface IInputRootObject { - inputLabel: string; - inputName: string; - customValidation: ICustomValidation; - errors?: IErrors; - register?: UseFormRegister; - type?: string; -} -/** - * Input field component displays a text input in a form, with label. - * The various properties of the input field can be determined with the props: - * @param {ICustomValidation} [customValidation] - the validation rules to apply to the input field - * @param {IErrors} errors - the form errors object provided by react-hook-form - * @param {string} inputLabel - used for the display label - * @param {string} inputName - the key of the value in the submitted data. Must be unique - * @param {UseFormRegister} register - register function from react-hook-form - * @param {boolean} [required=true] - whether or not this field is required. default true - * @param {string} [type='text'] - the input type. defaults to text - */ -export const InputField = ({ - customValidation, - inputLabel, - inputName, - type, -}: IInputRootObject) => { - const { register } = useFormContext(); - return ( -
- - -
- ); -}; -``` - -## File: src/components/Layout/Layout.component.tsx -```typescript -// Imports -import { ReactNode, useContext, useEffect } from 'react'; -import { useQuery } from '@apollo/client'; -// Components -import Header from '@/components/Header/Header.component'; -import PageTitle from './PageTitle.component'; -import Footer from '@/components/Footer/Footer.component'; -import Stickynav from '@/components/Footer/Stickynav.component'; -// State -import { CartContext } from '@/stores/CartProvider'; -// Utils -import { getFormattedCart } from '@/utils/functions/functions'; -// GraphQL -import { GET_CART } from '@/utils/gql/GQL_QUERIES'; -interface ILayoutProps { - children?: ReactNode; - title: string; -} -/** - * Renders layout for each page. Also passes along the title to the Header component. - * @function Layout - * @param {ReactNode} children - Children to be rendered by Layout component - * @param {TTitle} title - Title for the page. Is set in {title} - * @returns {JSX.Element} - Rendered component - */ -const Layout = ({ children, title }: ILayoutProps) => { - const { setCart } = useContext(CartContext); - const { data, refetch } = useQuery(GET_CART, { - notifyOnNetworkStatusChange: true, - onCompleted: () => { - // Update cart in the localStorage. - const updatedCart = getFormattedCart(data); - if (!updatedCart && !data?.cart?.contents?.nodes.length) { - // Should we clear the localStorage if we have no remote cart? - return; - } - localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart)); - // Update cart data in React Context. - setCart(updatedCart); - }, - }); - useEffect(() => { - refetch(); - }, [refetch]); - return ( -
-
- {title === 'Hjem' ? ( -
{children}
- ) : ( -
- -
{children}
-
- )} -
-
- -
-
- ); -}; -export default Layout; -``` - -## File: src/components/Layout/PageTitle.component.tsx -```typescript -interface IPageTitleProps { - title: string; -} -/** - * Renders page title for each page. - * @function PageTitle - * @param {string} title - Title for the page. Is set in {title} - * @returns {JSX.Element} - Rendered component - */ -const PageTitle = ({ title }: IPageTitleProps) => ( -
-
-

- {title} -

-
-
-); -export default PageTitle; -``` - -## File: src/components/LoadingSpinner/LoadingSpinner.component.tsx -```typescript -/** - * Loading spinner, shows while loading products or categories. - * Uses Styled-Components - */ -const LoadingSpinner = () => ( -
-
- - Laster ... -
-
-); -export default LoadingSpinner; -``` - -## File: src/components/Product/AddToCart.component.tsx -```typescript -// Imports -import { useContext, useState } from 'react'; -import { useQuery, useMutation } from '@apollo/client'; -import { v4 as uuidv4 } from 'uuid'; -// Components -import Button from '@/components/UI/Button.component'; -// State -import { CartContext } from '@/stores/CartProvider'; -// Utils -import { getFormattedCart } from '@/utils/functions/functions'; -// GraphQL -import { GET_CART } from '@/utils/gql/GQL_QUERIES'; -import { ADD_TO_CART } from '@/utils/gql/GQL_MUTATIONS'; -interface IImage { - __typename: string; - id: string; - uri: string; - title: string; - srcSet: string; - sourceUrl: string; -} -interface IVariationNode { - __typename: string; - name: string; -} -interface IAllPaColors { - __typename: string; - nodes: IVariationNode[]; -} -interface IAllPaSizes { - __typename: string; - nodes: IVariationNode[]; -} -export interface IVariationNodes { - __typename: string; - id: string; - databaseId: number; - name: string; - stockStatus: string; - stockQuantity: number; - purchasable: boolean; - onSale: boolean; - salePrice?: string; - regularPrice: string; -} -interface IVariations { - __typename: string; - nodes: IVariationNodes[]; -} -export interface IProduct { - __typename: string; - id: string; - databaseId: number; - averageRating: number; - slug: string; - description: string; - onSale: boolean; - image: IImage; - name: string; - salePrice?: string; - regularPrice: string; - price: string; - stockQuantity: number; - allPaColors?: IAllPaColors; - allPaSizes?: IAllPaSizes; - variations?: IVariations; -} -export interface IProductRootObject { - product: IProduct; - variationId?: number; - fullWidth?: boolean; -} -/** - * Handles the Add to cart functionality. - * Uses GraphQL for product data - * @param {IAddToCartProps} product // Product data - * @param {number} variationId // Variation ID - * @param {boolean} fullWidth // Whether the button should be full-width - */ -const AddToCart = ({ - product, - variationId, - fullWidth = false, -}: IProductRootObject) => { - const { setCart, isLoading: isCartLoading } = useContext(CartContext); - const [requestError, setRequestError] = useState(false); - const productId = product?.databaseId ? product?.databaseId : variationId; - const productQueryInput = { - clientMutationId: uuidv4(), // Generate a unique id. - productId, - }; - // Get cart data query - const { data, refetch } = useQuery(GET_CART, { - notifyOnNetworkStatusChange: true, - onCompleted: () => { - // Update cart in the localStorage. - const updatedCart = getFormattedCart(data); - if (!updatedCart) { - return; - } - localStorage.setItem('woocommerce-cart', JSON.stringify(updatedCart)); - // Update cart data in React Context. - setCart(updatedCart); - }, - }); - // Add to cart mutation - const [addToCart, { loading: addToCartLoading }] = useMutation(ADD_TO_CART, { - variables: { - input: productQueryInput, - }, - onCompleted: () => { - // Update the cart with new values in React context. - refetch(); - }, - onError: () => { - setRequestError(true); - }, - }); - const handleAddToCart = () => { - addToCart(); - // Refetch cart after 2 seconds - setTimeout(() => { - refetch(); - }, 2000); - }; - return ( - <> - - - ); -}; -export default AddToCart; -``` - -## File: src/components/Product/DisplayProducts.component.tsx -```typescript -/*eslint complexity: ["error", 20]*/ -import Link from 'next/link'; -import { v4 as uuidv4 } from 'uuid'; -import { filteredVariantPrice, paddedPrice } from '@/utils/functions/functions'; -interface Image { - __typename: string; - sourceUrl?: string; -} -interface Node { - __typename: string; - price: string; - regularPrice: string; - salePrice?: string; -} -interface Variations { - __typename: string; - nodes: Node[]; -} -interface RootObject { - __typename: string; - databaseId: number; - name: string; - onSale: boolean; - slug: string; - image: Image; - price: string; - regularPrice: string; - salePrice?: string; - variations: Variations; -} -interface IDisplayProductsProps { - products: RootObject[]; -} -/** - * Displays all of the products as long as length is defined. - * Does a map() over the props array and utilizes uuidv4 for unique key values. - * @function DisplayProducts - * @param {IDisplayProductsProps} products Products to render - * @returns {JSX.Element} - Rendered component - */ -const DisplayProducts = ({ products }: IDisplayProductsProps) => ( -
-
- {products ? ( - products.map( - ({ - databaseId, - name, - price, - regularPrice, - salePrice, - onSale, - slug, - image, - variations, - }) => { - // Add padding/empty character after currency symbol here - if (price) { - price = paddedPrice(price, 'kr'); - } - if (regularPrice) { - regularPrice = paddedPrice(regularPrice, 'kr'); - } - if (salePrice) { - salePrice = paddedPrice(salePrice, 'kr'); - } - return ( -
- -
- {image ? ( - {name} - ) : ( - {name} - )} -
- - - -
-

- {name} -

-
-
- -
- {onSale ? ( -
- - {variations && filteredVariantPrice(price, '')} - {!variations && salePrice} - - - {variations && filteredVariantPrice(price, 'right')} - {!variations && regularPrice} - -
- ) : ( - - {price} - - )} -
-
- ); - }, - ) - ) : ( -
- Ingen produkter funnet -
- )} -
-
-); -export default DisplayProducts; -``` - -## File: src/components/Product/ProductCard.component.tsx -```typescript -import Link from 'next/link'; -import Image from 'next/image'; -interface ProductCardProps { - databaseId: number; - name: string; - price: string; - slug: string; - image?: { - sourceUrl?: string; - }; -} -const ProductCard = ({ - databaseId, - name, - price, - slug, - image, -}: ProductCardProps) => { - return ( -
-
- - {image?.sourceUrl ? ( - {name} - ) : ( -
- No image -
- )} - -
- -
-

- {name} -

-
- -
- {price} -
-
- ); -}; -export default ProductCard; -``` - -## File: src/components/Product/ProductFilters.component.tsx -```typescript -import { Dispatch, SetStateAction } from 'react'; -import { Product, ProductType } from '@/types/product'; -import Button from '@/components/UI/Button.component'; -import Checkbox from '@/components/UI/Checkbox.component'; -import RangeSlider from '@/components/UI/RangeSlider.component'; -interface ProductFiltersProps { - selectedSizes: string[]; - setSelectedSizes: Dispatch>; - selectedColors: string[]; - setSelectedColors: Dispatch>; - priceRange: [number, number]; - setPriceRange: Dispatch>; - productTypes: ProductType[]; - toggleProductType: (id: string) => void; - products: Product[]; - resetFilters: () => void; -} -const ProductFilters = ({ - selectedSizes, - setSelectedSizes, - selectedColors, - setSelectedColors, - priceRange, - setPriceRange, - productTypes, - toggleProductType, - products, - resetFilters, -}: ProductFiltersProps) => { - // Get unique sizes from all products - const sizes = Array.from( - new Set( - products.flatMap( - (product: Product) => - product.allPaSizes?.nodes.map( - (node: { name: string }) => node.name, - ) || [], - ), - ), - ).sort((a, b) => a.localeCompare(b)); - // Get unique colors from all products - const availableColors = products - .flatMap((product: Product) => product.allPaColors?.nodes || []) - .filter((color, index, self) => - index === self.findIndex((c) => c.slug === color.slug) - ) - .sort((a, b) => a.name.localeCompare(b.name)); - const colors = availableColors.map((color) => ({ - name: color.name, - class: `bg-${color.slug}-500` - })); - const toggleSize = (size: string) => { - setSelectedSizes((prev) => - prev.includes(size) ? prev.filter((s) => s !== size) : [...prev, size], - ); - }; - const toggleColor = (color: string) => { - setSelectedColors((prev) => - prev.includes(color) ? prev.filter((c) => c !== color) : [...prev, color], - ); - }; - return ( -
-
-
-

PRODUKT TYPE

-
- {productTypes.map((type) => ( - toggleProductType(type.id)} - /> - ))} -
-
-
-

PRIS

- setPriceRange([priceRange[0], value])} - formatValue={(value) => `kr ${value}`} - /> -
-
-

STØRRELSE

-
- {sizes.map((size) => ( - - ))} -
-
-
-

FARGE

-
- {colors.map((color) => ( -
-
- -
-
- ); -}; -export default ProductFilters; -``` - -## File: src/components/Product/ProductList.component.tsx -```typescript -import { Product } from '@/types/product'; -import { useProductFilters } from '@/hooks/useProductFilters'; -import ProductCard from './ProductCard.component'; -import ProductFilters from './ProductFilters.component'; -interface ProductListProps { - products: Product[]; - title: string; -} -const ProductList = ({ products, title }: ProductListProps) => { - const { - sortBy, - setSortBy, - selectedSizes, - setSelectedSizes, - selectedColors, - setSelectedColors, - priceRange, - setPriceRange, - productTypes, - toggleProductType, - resetFilters, - filterProducts - } = useProductFilters(products); - const filteredProducts = filterProducts(products); - return ( -
- - {/* Main Content */} -
-
-

- {title} ({filteredProducts.length}) -

-
- - -
-
-
- {filteredProducts.map((product: Product) => ( - - ))} -
-
-
- ); -}; -export default ProductList; -``` - -## File: src/components/Product/SingleProduct.component.tsx -```typescript -// Imports -import { useState, useEffect } from 'react'; -// Utils -import { filteredVariantPrice, paddedPrice } from '@/utils/functions/functions'; -// Components -import AddToCart, { IProductRootObject } from './AddToCart.component'; -import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner.component'; -const SingleProduct = ({ product }: IProductRootObject) => { - const [isLoading, setIsLoading] = useState(true); - const [selectedVariation, setSelectedVariation] = useState(); - const placeholderFallBack = 'https://via.placeholder.com/600'; - let DESCRIPTION_WITHOUT_HTML; - useEffect(() => { - setIsLoading(false); - if (product.variations) { - const firstVariant = product.variations.nodes[0].databaseId; - setSelectedVariation(firstVariant); - } - }, [product.variations]); - let { description, image, name, onSale, price, regularPrice, salePrice } = - product; - // Add padding/empty character after currency symbol here - if (price) { - price = paddedPrice(price, 'kr'); - } - if (regularPrice) { - regularPrice = paddedPrice(regularPrice, 'kr'); - } - if (salePrice) { - salePrice = paddedPrice(salePrice, 'kr'); - } - // Strip out HTML from description - if (process.browser) { - DESCRIPTION_WITHOUT_HTML = new DOMParser().parseFromString( - description, - 'text/html', - ).body.textContent; - } - return ( -
- {isLoading ? ( -
-

Laster produkt ...

-
- -
- ) : ( -
-
- {/* Image Container */} -
-
- {name} -
-
- {/* Product Details Container */} -
-

- {name} -

- {/* Price Display */} -
- {onSale ? ( -
-

- {product.variations - ? filteredVariantPrice(price, '') - : salePrice} -

-

- {product.variations - ? filteredVariantPrice(price, 'right') - : regularPrice} -

-
- ) : ( -

{price}

- )} -
- {/* Description */} -

- {DESCRIPTION_WITHOUT_HTML} -

- {/* Stock Status */} - {Boolean(product.stockQuantity) && ( -
-
-

- {product.stockQuantity} på lager -

-
-
- )} - {/* Variations Select */} - {product.variations && ( -
- - -
- )} - {/* Add to Cart Button */} -
- {product.variations ? ( - - ) : ( - - )} -
-
-
-
- )} -
- ); -}; -export default SingleProduct; -``` - -## File: src/components/SVG/SVGMobileSearchIcon.component.tsx -```typescript -/** - * The SVG that we use for search in the navbar for mobile. - * Also includes logic for closing and opening the search form. - */ -const SVGMobileSearchIcon = () => { - const scrollToTop = () => { - window.scrollTo({ - top: 0, - behavior: 'smooth', - }); - }; - return ( -
- - - -
- ); -}; -export default SVGMobileSearchIcon; -``` - -## File: src/components/UI/Button.component.tsx -```typescript -import { ReactNode } from 'react'; -import Link from 'next/link'; -type TButtonVariant = 'primary' | 'secondary' | 'hero' | 'filter' | 'reset'; -interface IButtonProps { - handleButtonClick?: () => void; - buttonDisabled?: boolean; - variant?: TButtonVariant; - children?: ReactNode; - fullWidth?: boolean; - href?: string; - title?: string; - selected?: boolean; -} -/** - * Renders a clickable button - * @function Button - * @param {void} handleButtonClick - Handle button click - * @param {boolean?} buttonDisabled - Is button disabled? - * @param {TButtonVariant?} variant - Button variant - * @param {ReactNode} children - Children for button - * @param {boolean?} fullWidth - Whether the button should be full-width on mobile - * @param {boolean?} selected - Whether the button is in a selected state - * @returns {JSX.Element} - Rendered component - */ -const Button = ({ - handleButtonClick, - buttonDisabled, - variant = 'primary', - children, - fullWidth = false, - href, - title, - selected = false, -}: IButtonProps) => { - const getVariantClasses = (variant: TButtonVariant = 'primary') => { - switch (variant) { - case 'hero': - return 'inline-block px-8 py-4 text-sm tracking-wider uppercase bg-white text-gray-900 hover:bg-gray-400 hover:text-white hover:shadow-md'; - case 'filter': - return selected - ? 'px-3 py-1 border rounded bg-gray-900 text-white' - : 'px-3 py-1 border rounded hover:bg-gray-100 bg-white text-gray-900'; - case 'reset': - return 'w-full mt-8 py-2 px-4 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors'; - case 'secondary': - return 'px-2 lg:px-4 py-2 font-bold border border-gray-400 border-solid rounded text-white bg-red-500 hover:bg-red-600'; - default: // primary - return 'px-2 lg:px-4 py-2 font-bold border border-gray-400 border-solid rounded text-white bg-blue-500 hover:bg-blue-600'; - } - }; - const classes = `${getVariantClasses(variant)} ease-in-out transition-all duration-300 disabled:opacity-50 ${ - fullWidth ? 'w-full md:w-auto' : '' - }`; - if (href) { - return ( - - {children} - - ); - } - return ( - - ); -}; -export default Button; -``` - -## File: src/components/UI/Checkbox.component.tsx -```typescript -import { ChangeEvent } from 'react'; -interface ICheckboxProps { - id: string; - label: string; - checked: boolean; - onChange: (e: ChangeEvent) => void; -} -/** - * A reusable checkbox component with a label - * @function Checkbox - * @param {string} id - Unique identifier for the checkbox - * @param {string} label - Label text to display next to the checkbox - * @param {boolean} checked - Whether the checkbox is checked - * @param {function} onChange - Handler for when the checkbox state changes - * @returns {JSX.Element} - Rendered component - */ -const Checkbox = ({ id, label, checked, onChange }: ICheckboxProps) => { - return ( - - ); -}; -export default Checkbox; -``` - -## File: src/components/UI/RangeSlider.component.tsx -```typescript -import { ChangeEvent } from 'react'; -interface IRangeSliderProps { - id: string; - label: string; - min: number; - max: number; - value: number; - onChange: (value: number) => void; - startValue?: number; - formatValue?: (value: number) => string; -} -/** - * A reusable range slider component with labels - * @function RangeSlider - * @param {string} id - Unique identifier for the slider - * @param {string} label - Accessible label for the slider - * @param {number} min - Minimum value of the range - * @param {number} max - Maximum value of the range - * @param {number} value - Current value of the slider - * @param {function} onChange - Handler for when the slider value changes - * @param {number} startValue - Optional starting value to display (defaults to min) - * @param {function} formatValue - Optional function to format the displayed values - * @returns {JSX.Element} - Rendered component - */ -const RangeSlider = ({ - id, - label, - min, - max, - value, - onChange, - startValue = min, - formatValue = (val: number) => val.toString(), -}: IRangeSliderProps) => { - const handleChange = (e: ChangeEvent) => { - onChange(parseInt(e.target.value)); - }; - return ( -
- - -
- {formatValue(startValue)} - {formatValue(value)} -
-
- ); -}; -export default RangeSlider; -``` - -## File: src/components/User/UserRegistration.component.tsx -```typescript -import { useState } from 'react'; -import { useMutation } from '@apollo/client'; -import { useForm, FormProvider } from 'react-hook-form'; -import { CREATE_USER } from '../../utils/gql/GQL_MUTATIONS'; -import { InputField } from '../Input/InputField.component'; -import LoadingSpinner from '../LoadingSpinner/LoadingSpinner.component'; -import Button from '../UI/Button.component'; -interface IRegistrationData { - username: string; - email: string; - password: string; - firstName: string; - lastName: string; -} -/** - * User registration component that handles WooCommerce customer creation - * @function UserRegistration - * @returns {JSX.Element} - Rendered component with registration form - */ -const UserRegistration = () => { - const methods = useForm(); - const [registerUser, { loading, error }] = useMutation(CREATE_USER); - const [registrationCompleted, setRegistrationCompleted] = useState(false); - const onSubmit = async (data: IRegistrationData) => { - try { - const response = await registerUser({ - variables: data, - }); - const customer = response.data?.registerCustomer?.customer; - if (customer) { - setRegistrationCompleted(true); - } else { - throw new Error('Failed to register customer'); - } - } catch (err: unknown) { - console.error('Registration error'); - } - }; - if (registrationCompleted) { - return ( -
-

- Registrering vellykket! -

-

Du kan nå logge inn med din konto.

-
- ); - } - return ( -
- -
-
- - - - - - {error && ( -
- {error.message} -
- )} -
-
- -
-
-
-
-
-
- ); -}; -export default UserRegistration; -``` - -## File: src/hooks/useProductFilters.ts -```typescript -import { useState } from 'react'; -import { Product, ProductType } from '@/types/product'; -import { getUniqueProductTypes } from '@/utils/functions/productUtils'; -export const useProductFilters = (products: Product[]) => { - const [sortBy, setSortBy] = useState('popular'); - const [selectedSizes, setSelectedSizes] = useState([]); - const [selectedColors, setSelectedColors] = useState([]); - const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]); - const [productTypes, setProductTypes] = useState(() => - products ? getUniqueProductTypes(products) : [], - ); - const toggleProductType = (id: string) => { - setProductTypes((prev) => - prev.map((type) => - type.id === id ? { ...type, checked: !type.checked } : type, - ), - ); - }; - const resetFilters = () => { - setSelectedSizes([]); - setSelectedColors([]); - setPriceRange([0, 1000]); - setProductTypes((prev) => - prev.map((type) => ({ ...type, checked: false })), - ); - }; - const filterProducts = (products: Product[]) => { - const filtered = products?.filter((product: Product) => { - // Filter by price - const productPrice = parseFloat(product.price.replace(/[^0-9.]/g, '')); - const withinPriceRange = - productPrice >= priceRange[0] && productPrice <= priceRange[1]; - if (!withinPriceRange) return false; - // Filter by product type - const selectedTypes = productTypes - .filter((t) => t.checked) - .map((t) => t.name.toLowerCase()); - if (selectedTypes.length > 0) { - const productCategories = - product.productCategories?.nodes.map((cat) => - cat.name.toLowerCase(), - ) || []; - if (!selectedTypes.some((type) => productCategories.includes(type))) - return false; - } - // Filter by size - if (selectedSizes.length > 0) { - const productSizes = - product.allPaSizes?.nodes.map((node) => node.name) || []; - if (!selectedSizes.some((size) => productSizes.includes(size))) - return false; - } - // Filter by color - if (selectedColors.length > 0) { - const productColors = - product.allPaColors?.nodes.map((node) => node.name) || []; - if (!selectedColors.some((color) => productColors.includes(color))) - return false; - } - return true; - }); - // Sort products - return [...(filtered || [])].sort((a, b) => { - const priceA = parseFloat(a.price.replace(/[^0-9.]/g, '')); - const priceB = parseFloat(b.price.replace(/[^0-9.]/g, '')); - switch (sortBy) { - case 'price-low': - return priceA - priceB; - case 'price-high': - return priceB - priceA; - case 'newest': - return b.databaseId - a.databaseId; - default: // 'popular' - return 0; - } - }); - }; - return { - sortBy, - setSortBy, - selectedSizes, - setSelectedSizes, - selectedColors, - setSelectedColors, - priceRange, - setPriceRange, - productTypes, - toggleProductType, - resetFilters, - filterProducts, - }; -}; -``` - -## File: src/pages/kategori/[slug].tsx -```typescript -import { withRouter } from 'next/router'; -// Components -import Layout from '@/components/Layout/Layout.component'; -import DisplayProducts from '@/components/Product/DisplayProducts.component'; -import client from '@/utils/apollo/ApolloClient'; -import { GET_PRODUCTS_FROM_CATEGORY } from '@/utils/gql/GQL_QUERIES'; -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -/** - * Display a single product with dynamic pretty urls - */ -const Produkt = ({ - categoryName, - products, -}: InferGetServerSidePropsType) => { - return ( - - {products ? ( - - ) : ( -
Laster produkt ...
- )} -
- ); -}; -export default withRouter(Produkt); -export const getServerSideProps: GetServerSideProps = async ({ - query: { id }, -}) => { - const res = await client.query({ - query: GET_PRODUCTS_FROM_CATEGORY, - variables: { id }, - }); - return { - props: { - categoryName: res.data.productCategory.name, - products: res.data.productCategory.products.nodes, - }, - }; -}; -``` - -## File: src/pages/produkt/[slug].tsx -```typescript -// Imports -import { withRouter } from 'next/router'; -// Components -import SingleProduct from '@/components/Product/SingleProduct.component'; -import Layout from '@/components/Layout/Layout.component'; -// Utilities -import client from '@/utils/apollo/ApolloClient'; -// Types -import type { - NextPage, - GetServerSideProps, - InferGetServerSidePropsType, -} from 'next'; -// GraphQL -import { GET_SINGLE_PRODUCT } from '@/utils/gql/GQL_QUERIES'; -/** - * Display a single product with dynamic pretty urls - * @function Produkt - * @param {InferGetServerSidePropsType} products - * @returns {JSX.Element} - Rendered component - */ -const Produkt: NextPage = ({ - product, - networkStatus, -}: InferGetServerSidePropsType) => { - const hasError = networkStatus === '8'; - return ( - - {product ? ( - - ) : ( -
Laster produkt ...
- )} - {hasError && ( -
- Feil under lasting av produkt ... -
- )} -
- ); -}; -export default withRouter(Produkt); -export const getServerSideProps: GetServerSideProps = async ({ - query: { id }, -}) => { - const { data, loading, networkStatus } = await client.query({ - query: GET_SINGLE_PRODUCT, - variables: { id }, - }); - return { - props: { product: data.product, loading, networkStatus }, - }; -}; -``` - -## File: src/pages/_app.tsx -```typescript -// Imports -import Router from 'next/router'; -import NProgress from 'nprogress'; -import { ApolloProvider } from '@apollo/client'; -// State import -import { CartProvider } from '@/stores/CartProvider'; -import client from '@/utils/apollo/ApolloClient'; -// Types -import type { AppProps } from 'next/app'; -// Styles -import '@/styles/globals.css'; -import 'nprogress/nprogress.css'; -// NProgress -Router.events.on('routeChangeStart', () => NProgress.start()); -Router.events.on('routeChangeComplete', () => NProgress.done()); -Router.events.on('routeChangeError', () => NProgress.done()); -function MyApp({ Component, pageProps }: AppProps) { - return ( - - - - - - ); -} -export default MyApp; -``` - -## File: src/pages/_document.tsx -```typescript -import { Html, Head, Main, NextScript } from 'next/document'; -export default function Document() { - return ( - - - - - -
- - - - ); -} -``` - -## File: src/pages/handlekurv.tsx -```typescript -// Components -import Layout from '@/components/Layout/Layout.component'; -import CartContents from '@/components/Cart/CartContents.component'; -// Types -import type { NextPage } from 'next'; -const Handlekurv: NextPage = () => ( - - - -); -export default Handlekurv; -``` - -## File: src/pages/index.tsx -```typescript -// Components -import Hero from '@/components/Index/Hero.component'; -import DisplayProducts from '@/components/Product/DisplayProducts.component'; -import Layout from '@/components/Layout/Layout.component'; -// Utilities -import client from '@/utils/apollo/ApolloClient'; -// Types -import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next'; -// GraphQL -import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES'; -/** - * Main index page - * @function Index - * @param {InferGetStaticPropsType} products - * @returns {JSX.Element} - Rendered component - */ -const Index: NextPage = ({ - products, -}: InferGetStaticPropsType) => ( - - - {products && } - -); -export default Index; -export const getStaticProps: GetStaticProps = async () => { - const { data, loading, networkStatus } = await client.query({ - query: FETCH_ALL_PRODUCTS_QUERY, - }); - return { - props: { - products: data.products.nodes, - loading, - networkStatus, - }, - revalidate: 60, - }; -}; -``` - -## File: src/pages/kasse.tsx -```typescript -// Components -import Layout from '@/components/Layout/Layout.component'; -import CheckoutForm from '@/components/Checkout/CheckoutForm.component'; -// Types -import type { NextPage } from 'next'; -const Kasse: NextPage = () => ( - - - -); -export default Kasse; -``` - -## File: src/pages/kategorier.tsx -```typescript -import { NextPage, InferGetStaticPropsType, GetStaticProps } from 'next'; -import Categories from '@/components/Category/Categories.component'; -import Layout from '@/components/Layout/Layout.component'; -import client from '@/utils/apollo/ApolloClient'; -import { FETCH_ALL_CATEGORIES_QUERY } from '@/utils/gql/GQL_QUERIES'; -/** - * Category page displays all of the categories - */ -const Kategorier: NextPage = ({ - categories, -}: InferGetStaticPropsType) => ( - - {categories && } - -); -export default Kategorier; -export const getStaticProps: GetStaticProps = async () => { - const result = await client.query({ - query: FETCH_ALL_CATEGORIES_QUERY, - }); - return { - props: { - categories: result.data.productCategories.nodes, - }, - revalidate: 10, - }; -}; -``` - -## File: src/pages/produkter.tsx -```typescript -import Head from 'next/head'; -import Layout from '@/components/Layout/Layout.component'; -import ProductList from '@/components/Product/ProductList.component'; -import client from '@/utils/apollo/ApolloClient'; -import { FETCH_ALL_PRODUCTS_QUERY } from '@/utils/gql/GQL_QUERIES'; -import type { NextPage, GetStaticProps, InferGetStaticPropsType } from 'next'; -const Produkter: NextPage = ({ - products, - loading, -}: InferGetStaticPropsType) => { - if (loading) - return ( - -
-
-
-
- ); - if (!products) - return ( - -
-

Ingen produkter funnet

-
-
- ); - return ( - - - Produkter | WooCommerce Next.js - -
- -
-
- ); -}; -export default Produkter; -export const getStaticProps: GetStaticProps = async () => { - const { data, loading, networkStatus } = await client.query({ - query: FETCH_ALL_PRODUCTS_QUERY, - }); - return { - props: { - products: data.products.nodes, - loading, - networkStatus, - }, - revalidate: 60, - }; -}; -``` - -## File: src/stores/CartProvider.tsx -```typescript -import React, { - useState, - useEffect, - createContext, - useMemo, -} from 'react'; -interface ICartProviderProps { - children: React.ReactNode; -} -interface Image { - sourceUrl?: string; - srcSet?: string; - title: string; -} -export interface Product { - cartKey: string; - name: string; - qty: number; - price: number; - totalPrice: string; - image: Image; - productId: number; -} -export interface RootObject { - products: Product[]; - totalProductsCount: number; - totalProductsPrice: number; -} -export type TRootObject = RootObject | string | null | undefined; -export type TRootObjectNull = RootObject | null | undefined; -interface ICartContext { - cart: RootObject | null | undefined; - setCart: React.Dispatch>; - updateCart: (newCart: RootObject) => void; - isLoading: boolean; -} -const CartState: ICartContext = { - cart: null, - setCart: () => {}, - updateCart: () => {}, - isLoading: true, -}; -export const CartContext = createContext(CartState); -/** - * Provides a global application context for the entire application with the cart contents - */ -export const CartProvider = ({ children }: ICartProviderProps) => { - const [cart, setCart] = useState(); - const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - // Check if we are client-side before we access the localStorage - if (typeof window !== 'undefined') { - const localCartData = localStorage.getItem('woocommerce-cart'); - if (localCartData) { - const cartData: RootObject = JSON.parse(localCartData); - setCart(cartData); - } - setIsLoading(false); - } - }, []); - const updateCart = (newCart: RootObject) => { - setCart(newCart); - if (typeof window !== 'undefined') { - localStorage.setItem('woocommerce-cart', JSON.stringify(newCart)); - } - }; - const contextValue = useMemo(() => { - return { cart, setCart, updateCart, isLoading }; - }, [cart, isLoading]); - return ( - - {children} - - ); -}; -``` - -## File: src/styles/algolia.min.css -```css -.ais-Breadcrumb-list,.ais-CurrentRefinements-list,.ais-HierarchicalMenu-list,.ais-Hits-list,.ais-InfiniteHits-list,.ais-InfiniteResults-list,.ais-Menu-list,.ais-NumericMenu-list,.ais-Pagination-list,.ais-RatingMenu-list,.ais-RefinementList-list,.ais-Results-list,.ais-ToggleRefinement-list{margin:0;padding:0;list-style:none}.ais-ClearRefinements-button,.ais-CurrentRefinements-delete,.ais-CurrentRefinements-reset,.ais-GeoSearch-redo,.ais-GeoSearch-reset,.ais-HierarchicalMenu-showMore,.ais-InfiniteHits-loadMore,.ais-InfiniteHits-loadPrevious,.ais-InfiniteResults-loadMore,.ais-Menu-showMore,.ais-RangeInput-submit,.ais-RefinementList-showMore,.ais-SearchBox-reset,.ais-SearchBox-submit,.ais-VoiceSearch-button{padding:0;overflow:visible;font:inherit;line-height:normal;color:inherit;background:none;border:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ais-ClearRefinements-button::-moz-focus-inner,.ais-CurrentRefinements-delete::-moz-focus-inner,.ais-CurrentRefinements-reset::-moz-focus-inner,.ais-GeoSearch-redo::-moz-focus-inner,.ais-GeoSearch-reset::-moz-focus-inner,.ais-HierarchicalMenu-showMore::-moz-focus-inner,.ais-InfiniteHits-loadMore::-moz-focus-inner,.ais-InfiniteHits-loadPrevious::-moz-focus-inner,.ais-InfiniteResults-loadMore::-moz-focus-inner,.ais-Menu-showMore::-moz-focus-inner,.ais-RangeInput-submit::-moz-focus-inner,.ais-RefinementList-showMore::-moz-focus-inner,.ais-SearchBox-reset::-moz-focus-inner,.ais-SearchBox-submit::-moz-focus-inner,.ais-VoiceSearch-button::-moz-focus-inner{padding:0;border:0}.ais-ClearRefinements-button[disabled],.ais-CurrentRefinements-delete[disabled],.ais-CurrentRefinements-reset[disabled],.ais-GeoSearch-redo[disabled],.ais-GeoSearch-reset[disabled],.ais-HierarchicalMenu-showMore[disabled],.ais-InfiniteHits-loadMore[disabled],.ais-InfiniteHits-loadPrevious[disabled],.ais-InfiniteResults-loadMore[disabled],.ais-Menu-showMore[disabled],.ais-RangeInput-submit[disabled],.ais-RefinementList-showMore[disabled],.ais-SearchBox-reset[disabled],.ais-SearchBox-submit[disabled],.ais-VoiceSearch-button[disabled]{cursor:default}.ais-Breadcrumb-item,.ais-Breadcrumb-list,.ais-Pagination-list,.ais-PoweredBy,.ais-RangeInput-form,.ais-RatingMenu-link{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.ais-GeoSearch,.ais-GeoSearch-map{height:100%}.ais-HierarchicalMenu-list .ais-HierarchicalMenu-list{margin-left:1em}.ais-PoweredBy-logo{display:block;height:1.2em;width:auto}.ais-RatingMenu-starIcon{display:block;width:20px;height:20px}.ais-SearchBox-input::-ms-clear,.ais-SearchBox-input::-ms-reveal{display:none;width:0;height:0}.ais-SearchBox-input::-webkit-search-cancel-button,.ais-SearchBox-input::-webkit-search-decoration,.ais-SearchBox-input::-webkit-search-results-button,.ais-SearchBox-input::-webkit-search-results-decoration{display:none}.ais-RangeSlider .rheostat{overflow:visible;margin-top:40px;margin-bottom:40px}.ais-RangeSlider .rheostat-background{height:6px;top:0;width:100%}.ais-RangeSlider .rheostat-handle{margin-left:-12px;top:-7px}.ais-RangeSlider .rheostat-background{position:relative;background-color:#fff;border:1px solid #aaa}.ais-RangeSlider .rheostat-progress{position:absolute;top:1px;height:4px;background-color:#333}.rheostat-handle{position:relative;z-index:1;width:20px;height:20px;background-color:#fff;border:1px solid #333;border-radius:50%;cursor:-webkit-grab;cursor:grab}.rheostat-marker{margin-left:-1px;position:absolute;width:1px;height:5px;background-color:#aaa}.rheostat-marker--large{height:9px}.rheostat-value{padding-top:15px}.rheostat-tooltip,.rheostat-value{margin-left:50%;position:absolute;text-align:center;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.rheostat-tooltip{top:-22px} -``` - -## File: src/styles/animate.min.css -```css -@charset "UTF-8"; -/*! - * animate.css - https://animate.style/ - * Version - 4.0.0 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2020 Animate.css - */:root{--animate-duration:1s;--animate-delay:1s;--animate-repeat:1}.animate__animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-duration:var(--animate-duration);animation-duration:var(--animate-duration);-webkit-animation-fill-mode:both;animation-fill-mode:both}.animate__animated.animate__infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animate__animated.animate__repeat-1{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-iteration-count:var(--animate-repeat);animation-iteration-count:var(--animate-repeat)}.animate__animated.animate__repeat-2{-webkit-animation-iteration-count:2;animation-iteration-count:2;-webkit-animation-iteration-count:calc(var(--animate-repeat)*2);animation-iteration-count:calc(var(--animate-repeat)*2)}.animate__animated.animate__repeat-3{-webkit-animation-iteration-count:3;animation-iteration-count:3;-webkit-animation-iteration-count:calc(var(--animate-repeat)*3);animation-iteration-count:calc(var(--animate-repeat)*3)}.animate__animated.animate__delay-1s{-webkit-animation-delay:1s;animation-delay:1s;-webkit-animation-delay:var(--animate-delay);animation-delay:var(--animate-delay)}.animate__animated.animate__delay-2s{-webkit-animation-delay:2s;animation-delay:2s;-webkit-animation-delay:calc(var(--animate-delay)*2);animation-delay:calc(var(--animate-delay)*2)}.animate__animated.animate__delay-3s{-webkit-animation-delay:3s;animation-delay:3s;-webkit-animation-delay:calc(var(--animate-delay)*3);animation-delay:calc(var(--animate-delay)*3)}.animate__animated.animate__delay-4s{-webkit-animation-delay:4s;animation-delay:4s;-webkit-animation-delay:calc(var(--animate-delay)*4);animation-delay:calc(var(--animate-delay)*4)}.animate__animated.animate__delay-5s{-webkit-animation-delay:5s;animation-delay:5s;-webkit-animation-delay:calc(var(--animate-delay)*5);animation-delay:calc(var(--animate-delay)*5)}.animate__animated.animate__faster{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-duration:calc(var(--animate-duration)/2);animation-duration:calc(var(--animate-duration)/2)}.animate__animated.animate__fast{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-duration:calc(var(--animate-duration)*0.8);animation-duration:calc(var(--animate-duration)*0.8)}.animate__animated.animate__slow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2)}.animate__animated.animate__slower{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-duration:calc(var(--animate-duration)*3);animation-duration:calc(var(--animate-duration)*3)}@media (prefers-reduced-motion:reduce),print{.animate__animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}.animate__animated[class*=Out]{opacity:0}}@-webkit-keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}@keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}.animate__bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.animate__flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__pulse{-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.animate__shakeX{-webkit-animation-name:shakeX;animation-name:shakeX}@-webkit-keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}@keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}.animate__shakeY{-webkit-animation-name:shakeY;animation-name:shakeY}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.animate__headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.animate__swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.animate__jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}.animate__heartBeat{-webkit-animation-name:heartBeat;animation-name:heartBeat;-webkit-animation-duration:1.3s;animation-duration:1.3s;-webkit-animation-duration:calc(var(--animate-duration)*1.3);animation-duration:calc(var(--animate-duration)*1.3);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInDown{-webkit-animation-name:backInDown;animation-name:backInDown}@-webkit-keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInLeft{-webkit-animation-name:backInLeft;animation-name:backInLeft}@-webkit-keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInRight{-webkit-animation-name:backInRight;animation-name:backInRight}@-webkit-keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInUp{-webkit-animation-name:backInUp;animation-name:backInUp}@-webkit-keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}@keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}.animate__backOutDown{-webkit-animation-name:backOutDown;animation-name:backOutDown}@-webkit-keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}@keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}.animate__backOutLeft{-webkit-animation-name:backOutLeft;animation-name:backOutLeft}@-webkit-keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}@keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}.animate__backOutRight{-webkit-animation-name:backOutRight;animation-name:backOutRight}@-webkit-keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}@keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}.animate__backOutUp{-webkit-animation-name:backOutUp;animation-name:backOutUp}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__bounceIn{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.animate__bounceOut{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}.animate__bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}.animate__bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}.animate__bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}.animate__bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate__fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopLeft{-webkit-animation-name:fadeInTopLeft;animation-name:fadeInTopLeft}@-webkit-keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopRight{-webkit-animation-name:fadeInTopRight;animation-name:fadeInTopRight}@-webkit-keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomLeft{-webkit-animation-name:fadeInBottomLeft;animation-name:fadeInBottomLeft}@-webkit-keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomRight{-webkit-animation-name:fadeInBottomRight;animation-name:fadeInBottomRight}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.animate__fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.animate__fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.animate__fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.animate__fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.animate__fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}@keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}.animate__fadeOutTopLeft{-webkit-animation-name:fadeOutTopLeft;animation-name:fadeOutTopLeft}@-webkit-keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}@keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}.animate__fadeOutTopRight{-webkit-animation-name:fadeOutTopRight;animation-name:fadeOutTopRight}@-webkit-keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}@keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}.animate__fadeOutBottomRight{-webkit-animation-name:fadeOutBottomRight;animation-name:fadeOutBottomRight}@-webkit-keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}@keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}.animate__fadeOutBottomLeft{-webkit-animation-name:fadeOutBottomLeft;animation-name:fadeOutBottomLeft}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animate__animated.animate__flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.animate__flipOutX{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.animate__flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInRight{-webkit-animation-name:lightSpeedInRight;animation-name:lightSpeedInRight;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInLeft{-webkit-animation-name:lightSpeedInLeft;animation-name:lightSpeedInLeft;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.animate__lightSpeedOutRight{-webkit-animation-name:lightSpeedOutRight;animation-name:lightSpeedOutRight;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}@keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}.animate__lightSpeedOutLeft{-webkit-animation-name:lightSpeedOutLeft;animation-name:lightSpeedOutLeft;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.animate__rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.animate__rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.animate__rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.animate__hinge{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2);-webkit-animation-name:hinge;animation-name:hinge;-webkit-transform-origin:top left;transform-origin:top left}@-webkit-keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.animate__jackInTheBox{-webkit-animation-name:jackInTheBox;animation-name:jackInTheBox}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.animate__rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.animate__zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}.animate__zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}.animate__zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft;-webkit-transform-origin:left center;transform-origin:left center}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}.animate__zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight;-webkit-transform-origin:right center;transform-origin:right center}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} -``` - -## File: src/styles/globals.css -```css -@tailwind base; -@tailwind components; -@tailwind utilities; -#cart-div { - width: 300px; -} -#closeXsearch { - padding-left: 5px; -} -#close-cart-p { - margin-top: 2px; -} -#mobile-search-close-p { - margin-top: -20px; -} -/* Fix Algolia mobile searchbox design issues */ -#mobilesearchdiv { - position: absolute; - height: 200px; -} -.ais-SearchBox-submit { - width: 48px; -} -.ais-SearchBox-submitIcon { - display: none; -} -.ais-SearchBox-reset { - margin-left: 10px; -} -/* Fix Chrome padding issue */ -.ais-SearchBox-input[type='search']::-webkit-search-cancel-button { - display: none; -} -``` - -## File: src/tests/Index/Index.spec.ts -```typescript -import { test, expect } from '@playwright/test'; -test.describe('Forside', () => { - test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:3000'); - }); - test('Har h1 innhold på forsiden', async ({ page }) => { - const h1 = await page.locator('h1'); - const count = await h1.count(); - await expect(count).toBeGreaterThan(0); - }); -}); -``` - -## File: src/tests/Produkter/Produkter.spec.ts -```typescript -import { test, expect } from '@playwright/test'; -test.describe('Produkter', () => { - test.beforeEach(async ({ page }) => { - await page.goto('http://localhost:3000'); - }); - test('Test at vi kan kjøpe produktet', async ({ page }) => { - await page.getByRole('link', { name: 'Test simple' }).first().click(); - // Expects the URL to contain test-simple - await page.waitForURL('http://localhost:3000/produkt/test-simple?id=29', { - waitUntil: 'networkidle', - }); - await expect(page).toHaveURL(/.*simple/); - await expect(page.getByRole('button', { name: 'KJØP' })).toBeVisible(); - await page.getByRole('button', { name: 'KJØP' }).click(); - await page.locator('#header').getByText('1').waitFor(); - await expect(page.locator('#header').getByText('1')).toBeVisible({ - timeout: 5000, - }); - await page.getByRole('link', { name: 'Handlekurv' }).click(); - await page.locator('section').filter({ hasText: 'Handlekurv' }).waitFor(); - // Check that that Handlekurv is visible - await expect( - page.locator('section').filter({ hasText: 'Handlekurv' }), - ).toBeVisible(); - // Check that we can go to Kasse - await page.getByRole('button', { name: 'GÅ TIL KASSE' }).click(); - await page.waitForURL('http://localhost:3000/kasse', { - waitUntil: 'networkidle', - }); - await expect( - page.locator('section').filter({ hasText: 'Kasse' }), - ).toBeVisible(); - // Check that we can type something in Billing fields - await page.getByPlaceholder('Etternavn').fill('testetternavn'); - await page.getByPlaceholder('Etternavn').waitFor(); - await expect(page.getByPlaceholder('Etternavn')).toHaveValue( - 'testetternavn', - ); - }); -}); -``` - -## File: src/tests/example.spec.txt -``` -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects the URL to contain intro. - await expect(page).toHaveURL(/.*intro/); -}); -``` - -## File: src/types/product.ts -```typescript -export interface Image { - __typename: string; - sourceUrl?: string; -} -export interface Node { - __typename: string; - price: string; - regularPrice: string; - salePrice?: string; -} -export interface ProductCategory { - name: string; - slug: string; -} -export interface ColorNode { - name: string; - slug: string; -} -export interface SizeNode { - name: string; -} -export interface AttributeNode { - name: string; - value: string; -} -export interface Product { - __typename: string; - databaseId: number; - name: string; - onSale: boolean; - slug: string; - image: Image; - price: string; - regularPrice: string; - salePrice?: string; - productCategories?: { - nodes: ProductCategory[]; - }; - allPaColors?: { - nodes: ColorNode[]; - }; - allPaSizes?: { - nodes: SizeNode[]; - }; - variations: { - nodes: Array<{ - price: string; - regularPrice: string; - salePrice?: string; - attributes?: { - nodes: AttributeNode[]; - }; - }>; - }; -} -export interface ProductType { - id: string; - name: string; - checked: boolean; -} -``` - -## File: src/utils/apollo/ApolloClient.js -```javascript -/*eslint complexity: ["error", 6]*/ -import { - ApolloClient, - InMemoryCache, - createHttpLink, - ApolloLink, -} from '@apollo/client'; -const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds -/** - * Middleware operation - * If we have a session token in localStorage, add it to the GraphQL request as a Session header. - */ -export const middleware = new ApolloLink((operation, forward) => { - /** - * If session data exist in local storage, set value as session header. - * Here we also delete the session if it is older than 7 days - */ - const sessionData = process.browser - ? JSON.parse(localStorage.getItem('woo-session')) - : null; - if (sessionData && sessionData.token && sessionData.createdTime) { - const { token, createdTime } = sessionData; - // Check if the token is older than 7 days - if (Date.now() - createdTime > SEVEN_DAYS) { - // If it is, delete it - localStorage.removeItem('woo-session'); - localStorage.setItem('woocommerce-cart', JSON.stringify({})); - } else { - // If it's not, use the token - operation.setContext(() => ({ - headers: { - 'woocommerce-session': `Session ${token}`, - }, - })); - } - } - return forward(operation); -}); -/** - * Afterware operation. - * - * This catches the incoming session token and stores it in localStorage, for future GraphQL requests. - */ -export const afterware = new ApolloLink((operation, forward) => - forward(operation).map((response) => { - /** - * Check for session header and update session in local storage accordingly. - */ - const context = operation.getContext(); - const { - response: { headers }, - } = context; - const session = headers.get('woocommerce-session'); - if (session && process.browser) { - if ('false' === session) { - // Remove session data if session destroyed. - localStorage.removeItem('woo-session'); - // Update session new data if changed. - } else if (!localStorage.getItem('woo-session')) { - localStorage.setItem( - 'woo-session', - JSON.stringify({ token: session, createdTime: Date.now() }), - ); - } - } - return response; - }), -); -const clientSide = typeof window === 'undefined'; -// Apollo GraphQL client. -const client = new ApolloClient({ - ssrMode: clientSide, - link: middleware.concat( - afterware.concat( - createHttpLink({ - uri: process.env.NEXT_PUBLIC_GRAPHQL_URL, - fetch, - }), - ), - ), - cache: new InMemoryCache(), -}); -export default client; -``` - -## File: src/utils/constants/INPUT_FIELDS.ts -```typescript -export const INPUT_FIELDS = [ - { - id: 0, - label: 'Fornavn', - name: 'firstName', - customValidation: { required: true, minlength: 4 }, - }, - { - id: 1, - label: 'Etternavn', - name: 'lastName', - customValidation: { required: true, minlength: 4 }, - }, - { - id: 2, - label: 'Adresse', - name: 'address1', - customValidation: { required: true, minlength: 4 }, - }, - { - id: 3, - label: 'Postnummer', - name: 'postcode', - customValidation: { required: true, minlength: 4, pattern: '[+0-9]{4,6}' }, - }, - { - id: 4, - label: 'Sted', - name: 'city', - customValidation: { required: true, minlength: 2 }, - }, - { - id: 5, - label: 'Epost', - name: 'email', - customValidation: { required: true, type: 'email' }, - }, - { - id: 6, - label: 'Telefon', - name: 'phone', - customValidation: { required: true, minlength: 8, pattern: '[+0-9]{8,12}' }, - }, -]; -``` - -## File: src/utils/constants/LINKS.ts -```typescript -interface ILinks { - id: number; - title: string; - href: string; -} -const LINKS: ILinks[] = [ - { - id: 0, - title: 'Hjem', - href: '/', - }, - { - id: 1, - title: 'Produkter', - href: '/produkter', - }, - { - id: 2, - title: 'Kategorier', - href: '/kategorier', - }, -]; -export default LINKS; -``` - -## File: src/utils/functions/functions.tsx -```typescript -/*eslint complexity: ["error", 20]*/ -import { v4 as uuidv4 } from 'uuid'; -import { RootObject, Product } from '@/stores/CartProvider'; -import { ChangeEvent } from 'react'; -import { IVariationNodes } from '@/components/Product/AddToCart.component'; -/* Interface for products*/ -export interface IImage { - __typename: string; - id: string; - sourceUrl?: string; - srcSet?: string; - altText: string; - title: string; -} -export interface IGalleryImages { - __typename: string; - nodes: IImage[]; -} -interface IProductNode { - __typename: string; - id: string; - databaseId: number; - name: string; - description: string; - type: string; - onSale: boolean; - slug: string; - averageRating: number; - reviewCount: number; - image: IImage; - galleryImages: IGalleryImages; - productId: number; -} -interface IProduct { - __typename: string; - node: IProductNode; -} -export interface IProductRootObject { - __typename: string; - key: string; - product: IProduct; - variation?: IVariationNodes; - quantity: number; - total: string; - subtotal: string; - subtotalTax: string; -} -type TUpdatedItems = { key: string; quantity: number }[]; -export interface IUpdateCartItem { - key: string; - quantity: number; -} -export interface IUpdateCartInput { - clientMutationId: string; - items: IUpdateCartItem[]; -} -export interface IUpdateCartVariables { - input: IUpdateCartInput; -} -export interface IUpdateCartRootObject { - variables: IUpdateCartVariables; -} -/* Interface for props */ -interface IFormattedCartProps { - cart: { contents: { nodes: IProductRootObject[] }; total: number }; -} -export interface ICheckoutDataProps { - firstName: string; - lastName: string; - address1: string; - address2: string; - city: string; - country: string; - state: string; - postcode: string; - email: string; - phone: string; - company: string; - paymentMethod: string; -} -/** - * Add empty character after currency symbol - * @param {string} price The price string that we input - * @param {string} symbol Currency symbol to add empty character/padding after - */ -export const paddedPrice = (price: string, symbol: string) => - price.split(symbol).join(`${symbol} `); -/** - * Shorten inputted string (usually product description) to a maximum of length - * @param {string} input The string that we input - * @param {number} length The length that we want to shorten the text to - */ -export const trimmedStringToLength = (input: string, length: number) => { - if (input.length > length) { - const subStr = input.substring(0, length); - return `${subStr}...`; - } - return input; -}; -/** - * Filter variant price. Changes "kr198.00 - kr299.00" to kr299.00 or kr198 depending on the side variable - * @param {String} side Which side of the string to return (which side of the "-" symbol) - * @param {String} price The inputted price that we need to convert - */ -export const filteredVariantPrice = (price: string, side: string) => { - if ('right' === side) { - return price.substring(price.length, price.indexOf('-')).replace('-', ''); - } - return price.substring(0, price.indexOf('-')).replace('-', ''); -}; -/** - * Returns cart data in the required format. - * @param {String} data Cart data - */ -export const getFormattedCart = (data: IFormattedCartProps) => { - const formattedCart: RootObject = { - products: [], - totalProductsCount: 0, - totalProductsPrice: 0, - }; - if (!data) { - return; - } - const givenProducts = data.cart.contents.nodes; - // Create an empty object. - formattedCart.products = []; - const product: Product = { - productId: 0, - cartKey: '', - name: '', - qty: 0, - price: 0, - totalPrice: '0', - image: { sourceUrl: '', srcSet: '', title: '' }, - }; - let totalProductsCount = 0; - let i = 0; - if (!givenProducts.length) { - return; - } - givenProducts.forEach(() => { - const givenProduct = givenProducts[Number(i)].product.node; - // Convert price to a float value - const convertedCurrency = givenProducts[Number(i)].total.replace( - /[^0-9.-]+/g, - '', - ); - product.productId = givenProduct.productId; - product.cartKey = givenProducts[Number(i)].key; - product.name = givenProduct.name; - product.qty = givenProducts[Number(i)].quantity; - product.price = Number(convertedCurrency) / product.qty; - product.totalPrice = givenProducts[Number(i)].total; - // Ensure we can add products without images to the cart - product.image = givenProduct.image.sourceUrl - ? { - sourceUrl: givenProduct.image.sourceUrl, - srcSet: givenProduct.image.srcSet, - title: givenProduct.image.title, - } - : { - sourceUrl: process.env.NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL, - srcSet: process.env.NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL, - title: givenProduct.name, - }; - totalProductsCount += givenProducts[Number(i)].quantity; - // Push each item into the products array. - formattedCart.products.push(product); - i++; - }); - formattedCart.totalProductsCount = totalProductsCount; - formattedCart.totalProductsPrice = data.cart.total; - return formattedCart; -}; -export const createCheckoutData = (order: ICheckoutDataProps) => ({ - clientMutationId: uuidv4(), - billing: { - firstName: order.firstName, - lastName: order.lastName, - address1: order.address1, - address2: order.address2, - city: order.city, - country: order.country, - state: order.state, - postcode: order.postcode, - email: order.email, - phone: order.phone, - company: order.company, - }, - shipping: { - firstName: order.firstName, - lastName: order.lastName, - address1: order.address1, - address2: order.address2, - city: order.city, - country: order.country, - state: order.state, - postcode: order.postcode, - email: order.email, - phone: order.phone, - company: order.company, - }, - shipToDifferentAddress: false, - paymentMethod: order.paymentMethod, - isPaid: false, - transactionId: 'fhggdfjgfi', -}); -/** - * Get the updated items in the below format required for mutation input. - * - * Creates an array in above format with the newQty (updated Qty ). - * - */ -export const getUpdatedItems = ( - products: IProductRootObject[], - newQty: number, - cartKey: string, -) => { - // Create an empty array. - const updatedItems: TUpdatedItems = []; - // Loop through the product array. - products.forEach((cartItem) => { - // If you find the cart key of the product user is trying to update, push the key and new qty. - if (cartItem.key === cartKey) { - updatedItems.push({ - key: cartItem.key, - quantity: newQty, - }); - // Otherwise just push the existing qty without updating. - } else { - updatedItems.push({ - key: cartItem.key, - quantity: cartItem.quantity, - }); - } - }); - // Return the updatedItems array with new Qtys. - return updatedItems; -}; -/* - * When user changes the quantity, update the cart in localStorage - * Also update the cart in the global Context - */ -export const handleQuantityChange = ( - event: ChangeEvent, - cartKey: string, - cart: IProductRootObject[], - updateCart: (variables: IUpdateCartRootObject) => void, - updateCartProcessing: boolean, -) => { - if (process.browser) { - event.stopPropagation(); - // Return if the previous update cart mutation request is still processing - if (updateCartProcessing || !cart) { - return; - } - // If the user tries to delete the count of product, set that to 1 by default ( This will not allow him to reduce it less than zero ) - const newQty = event.target.value ? parseInt(event.target.value, 10) : 1; - if (cart.length) { - const updatedItems = getUpdatedItems(cart, newQty, cartKey); - updateCart({ - variables: { - input: { - clientMutationId: uuidv4(), - items: updatedItems, - }, - }, - }); - } - } -}; -``` - -## File: src/utils/functions/productUtils.ts -```typescript -import { Product, ProductCategory, ProductType } from '@/types/product'; -export const getUniqueProductTypes = (products: Product[]): ProductType[] => { - // Use Map to ensure unique categories by slug - const categoryMap = new Map(); - products?.forEach((product) => { - product.productCategories?.nodes.forEach((cat: ProductCategory) => { - if (!categoryMap.has(cat.slug)) { - categoryMap.set(cat.slug, { - id: cat.slug, - name: cat.name, - checked: false, - }); - } - }); - }); - // Convert Map values to array and sort by name - return Array.from(categoryMap.values()).sort((a, b) => - a.name.localeCompare(b.name), - ); -}; -``` - -## File: src/utils/gql/GQL_MUTATIONS.ts -```typescript -import { gql } from '@apollo/client'; -export const CREATE_USER = gql` - mutation CreateUser( - $username: String! - $email: String! - $password: String! - $firstName: String - $lastName: String - ) { - registerCustomer( - input: { - username: $username - email: $email - password: $password - firstName: $firstName - lastName: $lastName - } - ) { - customer { - id - email - firstName - lastName - username - } - } - } -`; -export const ADD_TO_CART = gql` - mutation ($input: AddToCartInput!) { - addToCart(input: $input) { - cartItem { - key - product { - node { - id - databaseId - name - description - type - onSale - slug - averageRating - reviewCount - image { - id - sourceUrl - altText - } - galleryImages { - nodes { - id - sourceUrl - altText - } - } - } - } - variation { - node { - id - databaseId - name - description - type - onSale - price - regularPrice - salePrice - image { - id - sourceUrl - altText - } - attributes { - nodes { - id - attributeId - name - value - } - } - } - } - quantity - total - subtotal - subtotalTax - } - } - } -`; -export const CHECKOUT_MUTATION = gql` - mutation CHECKOUT_MUTATION($input: CheckoutInput!) { - checkout(input: $input) { - result - redirect - } - } -`; -export const UPDATE_CART = gql` - mutation ($input: UpdateItemQuantitiesInput!) { - updateItemQuantities(input: $input) { - items { - key - product { - node { - id - databaseId - name - description - type - onSale - slug - averageRating - reviewCount - image { - id - sourceUrl - altText - } - galleryImages { - nodes { - id - sourceUrl - altText - } - } - } - } - variation { - node { - id - databaseId - name - description - type - onSale - price - regularPrice - salePrice - image { - id - sourceUrl - altText - } - attributes { - nodes { - id - attributeId - name - value - } - } - } - } - quantity - total - subtotal - subtotalTax - } - removed { - key - product { - node { - id - databaseId - } - } - variation { - node { - id - databaseId - } - } - } - updated { - key - product { - node { - id - databaseId - } - } - variation { - node { - id - databaseId - } - } - } - } - } -`; -``` - -## File: src/utils/gql/GQL_QUERIES.ts -```typescript -import { gql } from '@apollo/client'; -export const GET_SINGLE_PRODUCT = gql` - query Product($id: ID!) { - product(id: $id, idType: DATABASE_ID) { - id - databaseId - averageRating - slug - description - onSale - image { - id - uri - title - srcSet - sourceUrl - } - name - ... on SimpleProduct { - salePrice - regularPrice - price - id - stockQuantity - } - ... on VariableProduct { - salePrice - regularPrice - price - id - allPaColors { - nodes { - name - } - } - allPaSizes { - nodes { - name - } - } - variations { - nodes { - id - databaseId - name - stockStatus - stockQuantity - purchasable - onSale - salePrice - regularPrice - } - } - } - ... on ExternalProduct { - price - id - externalUrl - } - ... on GroupProduct { - products { - nodes { - ... on SimpleProduct { - id - price - } - } - } - id - } - } - } -`; -/** - * Fetch first 4 products from a specific category - */ -export const FETCH_FIRST_PRODUCTS_FROM_HOODIES_QUERY = ` - query MyQuery { - products(first: 4, where: {category: "Hoodies"}) { - nodes { - productId - name - onSale - slug - image { - sourceUrl - } - ... on SimpleProduct { - price - regularPrice - salePrice - } - ... on VariableProduct { - price - regularPrice - salePrice - } - } - } -} - `; -/** - * Fetch first 200 Woocommerce products from GraphQL - */ -export const FETCH_ALL_PRODUCTS_QUERY = gql` - query MyQuery { - products(first: 50) { - nodes { - databaseId - name - onSale - slug - image { - sourceUrl - } - productCategories { - nodes { - name - slug - } - } - ... on SimpleProduct { - databaseId - price - regularPrice - salePrice - } - ... on VariableProduct { - databaseId - price - regularPrice - salePrice - allPaColors { - nodes { - name - slug - } - } - allPaSizes { - nodes { - name - } - } - variations { - nodes { - price - regularPrice - salePrice - attributes { - nodes { - name - value - } - } - } - } - } - } - } - } -`; -/** - * Fetch first 20 categories from GraphQL - */ -export const FETCH_ALL_CATEGORIES_QUERY = gql` - query Categories { - productCategories(first: 20) { - nodes { - id - name - slug - } - } - } -`; -export const GET_PRODUCTS_FROM_CATEGORY = gql` - query ProductsFromCategory($id: ID!) { - productCategory(id: $id) { - id - name - products(first: 50) { - nodes { - id - databaseId - onSale - averageRating - slug - description - image { - id - uri - title - srcSet - sourceUrl - } - name - ... on SimpleProduct { - salePrice - regularPrice - onSale - price - id - } - ... on VariableProduct { - salePrice - regularPrice - onSale - price - id - } - ... on ExternalProduct { - price - id - externalUrl - } - ... on GroupProduct { - products { - nodes { - ... on SimpleProduct { - id - price - } - } - } - id - } - } - } - } - } -`; -export const GET_CART = gql` - query GET_CART { - cart { - contents { - nodes { - key - product { - node { - id - databaseId - name - description - type - onSale - slug - averageRating - reviewCount - image { - id - sourceUrl - srcSet - altText - title - } - galleryImages { - nodes { - id - sourceUrl - srcSet - altText - title - } - } - } - } - variation { - node { - id - databaseId - name - description - type - onSale - price - regularPrice - salePrice - image { - id - sourceUrl - srcSet - altText - title - } - attributes { - nodes { - id - name - value - } - } - } - } - quantity - total - subtotal - subtotalTax - } - } - subtotal - subtotalTax - shippingTax - shippingTotal - total - totalTax - feeTax - feeTotal - discountTax - discountTotal - } - } -`; -``` - -## File: .codeclimate.yml -```yaml -version: "2" -checks: - argument-count: - config: - threshold: 4 - complex-logic: - config: - threshold: 4 - file-lines: - config: - threshold: 150 - method-complexity: - config: - threshold: 15 - method-count: - config: - threshold: 20 - method-lines: - config: - threshold: 150 - nested-control-flow: - config: - threshold: 4 - return-statements: - config: - threshold: 4 - similar-code: - config: - threshold: # language-specific defaults. an override will affect all languages. - identical-code: - config: - threshold: # language-specific defaults. an override will affect all languages. -exclude_patterns: - - "config/" - - "db/" - - "dist/" - - "features/" - - "**/node_modules/" - - "script/" - - "**/spec/" - - "**/test/" - - "**/tests/" - - "**/vendor/" - - "**/*.d.ts" -``` - -## File: .env.example -``` -NEXT_PUBLIC_GRAPHQL_URL="https://wordpress.url.com/graphql" -NEXT_PUBLIC_ALGOLIA_INDEX_NAME= "algolia" -NEXT_PUBLIC_PLACEHOLDER_SMALL_IMAGE_URL="https://res.cloudinary.com/placeholder-337_utsb7h.jpg" -NEXT_PUBLIC_PLACEHOLDER_LARGE_IMAGE_URL="https://via.placeholder.com/600" -NEXT_PUBLIC_ALGOLIA_APP_ID = "changeme" -NEXT_PUBLIC_ALGOLIA_PUBLIC_API_KEY = "changeme" -NODE_ENV="development" -``` - -## File: .eslintrc.json -```json -{ - "extends": ["next/core-web-vitals", "eslint:recommended"], - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "rules": { - "@next/next/no-img-element": "off", - "no-useless-escape": "off", - "@typescript-eslint/no-unused-vars": 1 - }, - "globals": { "JSX": true }, - "env": { - "browser": true, - "es6": true - } -} -``` - -## File: .gitignore -``` -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts -/test-results/ -/playwright-report/ -/playwright/.cache/ -src/pages/registrer.tsx -``` - -## File: .prettierrc -``` -{ - "semi": true, - "jsxBracketSameLine": false, - "singleQuote": true, - "jsxSingleQuote": false -} -``` - -## File: CODE_OF_CONDUCT.md -```markdown -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see - -``` - -## File: CONTRIBUTING.md -```markdown -# Contributing - -When contributing to this repository, please first discuss the change you wish to make via issue, -email, or any other method with the owners of this repository before making a change. - -Please note we have a code of conduct, please follow it in all your interactions with the project. - -## Pull Request Process - -1. Ensure any install or build dependencies are removed before the end of the layer when doing a - build. -2. Update the README.md with details of changes to the interface, this includes new environment - variables, exposed ports, useful file locations and container parameters. -3. Increase the version numbers in any examples files and the README.md to the new version that this - Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you - do not have permission to do that, you may request the second reviewer to merge it for you. - -## Code of Conduct - -### Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -### Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -### Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -### Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -### Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [INSERT EMAIL ADDRESS]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -### Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org - -[version]: http://contributor-covenant.org/version/1/4/ -``` - -## File: LICENSE -``` -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. -``` - -## File: next.config.js -```javascript -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'swewoocommerce.dfweb.no', - pathname: '**', - }, - { - protocol: 'https', - hostname: 'res.cloudinary.com', - pathname: '**', - }, - { - protocol: 'https', - hostname: 'via.placeholder.com', - pathname: '**', - }, - ], - }, -}; -module.exports = nextConfig; -``` - -## File: package.json -```json -{ - "name": "nextjs-woocommerce", - "version": "1.2.3", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint", - "format": "prettier --write \"**/*.{js,ts,tsx,json}\"", - "playwright": "npx playwright test", - "playwright:ui": "npx playwright test --ui", - "playwright:debug": "npx playwright test --debug", - "playwright:codegen": "playwright codegen", - "refresh": "rm -rf node_modules && rm package-lock.json && npm i && npm run format" - }, - "keywords": [ - "next.js", - "next", - "javascript", - "framer" - ], - "author": "w3bdesign", - "license": "ISC", - "dependencies": { - "@apollo/client": "^3.12.8", - "@types/react": "^19.0.8", - "algoliasearch": "^4.24.0", - "autoprefixer": "^10.4.20", - "framer-motion": "12.0.6", - "graphql": "^16.10.0", - "lodash": "^4.17.21", - "next": "15.1.6", - "nprogress": "^0.2.0", - "postcss": "^8.5.1", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-hook-form": "^7.54.2", - "react-instantsearch-dom": "^6.40.4", - "uuid": "^11.0.5" - }, - "devDependencies": { - "@playwright/test": "^1.50.0", - "@types/lodash": "^4.17.15", - "@types/node": "22.12.0", - "@types/nprogress": "^0.2.3", - "@types/react-instantsearch-dom": "^6.12.8", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.22.0", - "@typescript-eslint/parser": "^8.22.0", - "babel-plugin-styled-components": "^2.1.4", - "eslint-config-next": "^15.1.6", - "postcss-preset-env": "^10.1.3", - "prettier": "^3.4.2", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.3" - } -} -``` - -## File: playwright.config.ts -```typescript -import type { PlaywrightTestConfig } from '@playwright/test'; -import { devices } from '@playwright/test'; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -/** - * See https://playwright.dev/docs/test-configuration. - */ -const config: PlaywrightTestConfig = { - testDir: './src/tests', - /* Maximum time one test can run for. */ - timeout: 60 * 1000, // Increased to 60 seconds - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 30000, // Increased to 30 seconds - }, - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: Boolean(process.env.CI), - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : '100%', - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? 'github' : 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 30000, // Added 30 second timeout for actions - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:3000', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - viewport: { width: 2560, height: 1440 }, - }, - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, - }, - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, - // }, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, - ], - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - outputDir: 'test-results/', - /* Run your local dev server before starting the tests */ - webServer: { - reuseExistingServer: true, - command: 'npm run dev', - port: 3000, - timeout: 120000, // Added 2 minute timeout for server start - }, -}; -export default config; -``` - -## File: postcss.config.js -```javascript -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; -``` - -## File: README.md -```markdown -[![Playwright Tests](https://github.com/w3bdesign/nextjs-woocommerce/actions/workflows/playwright.yml/badge.svg)](https://github.com/w3bdesign/nextjs-woocommerce/actions/workflows/playwright.yml) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/29de6847b01142e6a0183988fc3df46a)](https://app.codacy.com/gh/w3bdesign/nextjs-woocommerce?utm_source=github.com&utm_medium=referral&utm_content=w3bdesign/nextjs-woocommerce&utm_campaign=Badge_Grade_Settings) -[![CodeFactor](https://www.codefactor.io/repository/github/w3bdesign/nextjs-woocommerce/badge)](https://www.codefactor.io/repository/github/w3bdesign/nextjs-woocommerce) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=w3bdesign_nextjs-woocommerce&metric=alert_status)](https://sonarcloud.io/dashboard?id=w3bdesign_nextjs-woocommerce) - -![bilde](https://github.com/user-attachments/assets/08047025-0950-472a-ae7d-932c7faee1db) - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=w3bdesign/nextjs-woocommerce&type=Date)](https://star-history.com/#w3bdesign/nextjs-woocommerce&Date) - -# Next.js Ecommerce site with WooCommerce backend - -## Live URL: - -## Table Of Contents (TOC) - -- [Installation](#Installation) -- [Features](#Features) -- [Issues](#Issues) -- [Troubleshooting](#Troubleshooting) -- [TODO](#TODO) -- [Future Improvements](SUGGESTIONS.md) - -## Installation - -1. Install and activate the following required plugins, in your WordPress plugin directory: - -- [woocommerce](https://wordpress.org/plugins/woocommerce) Ecommerce for WordPress. -- [wp-graphql](https://wordpress.org/plugins/wp-graphql) Exposes GraphQL for WordPress. -- [wp-graphql-woocommerce](https://github.com/wp-graphql/wp-graphql-woocommerce) Adds WooCommerce functionality to a WPGraphQL schema. -- [wp-algolia-woo-indexer](https://github.com/w3bdesign/wp-algolia-woo-indexer) WordPress plugin coded by me. Sends WooCommerce products to Algolia. Required for search to work. - -Optional plugin: - -- [headless-wordpress](https://github.com/w3bdesign/headless-wp) Disables the frontend so only the backend is accessible. (optional) - -The current release has been tested and is confirmed working with the following versions: - -- WordPress version 6.6.2 -- WooCommerce version 7.4.0 -- WP GraphQL version 1.13.8 -- WooGraphQL version 0.12.0 -- WPGraphQL CORS version 2.1 - -2. For debugging and testing, install either: - - (Firefox) - - (Chrome) - -3. Make sure WooCommerce has some products already - -4. Clone or fork the repo and modify `.env.example` and rename it to `.env` - - Then set the environment variables accordingly in Vercel or your preferred hosting solution. - - See - -5. Modify the values according to your setup - -6. Start the server with `npm run dev` - -7. Enable COD (Cash On Demand) payment method in WooCommerce - -8. Add a product to the cart - -9. Proceed to checkout (Gå til kasse) - -10. Fill in your details and place the order - -## Features - -- Next.js version 14.3.11 -- React version 18.3.1 -- Typescript -- Tests with Playwright -- Connect to Woocommerce GraphQL API and list name, price and display image for products -- Support for simple products and variable products -- Cart handling and checkout with WooCommerce (Cash On Delivery only for now) -- Algolia search (requires [algolia-woo-indexer](https://github.com/w3bdesign/algolia-woo-indexer)) -- Meets WCAG accessibility standards where possible -- Placeholder for products without images -- Apollo Client with GraphQL -- React Hook Form -- Native HTML5 form validation -- Animations with Framer motion, Styled components and Animate.css -- Loading spinner created with Styled Components -- Shows page load progress with Nprogress during navigation -- Fully responsive design -- Category and product listings -- Show stock status -- Pretty URLs with builtin Nextjs functionality -- Tailwind 3 for styling -- JSDoc comments -- Product filtering: - - Dynamic color filtering using Tailwind's color system - - Mobile-optimized filter layout - - Accessible form controls with ARIA labels - - Price range slider - - Size and color filters - - Product type categorization - - Sorting options (popularity, price, newest) - -## Troubleshooting - -### I am getting a cart undefined error or other GraphQL errors - -Check that you are using the 0.12.0 version of the [wp-graphql-woocommerce](https://github.com/wp-graphql/wp-graphql-woocommerce) plugin - -### The products page isn't loading - -Check the attributes of the products. Right now the application requires Size and Color. - -## Issues - -Overall the application is working as intended, but it has not been tested extensively in a production environment. -More testing and debugging is required before deploying it in a production environment. - -With that said, keep the following in mind: - -- Currently only simple products and variable products work without any issues. Other product types are not known to work. -- Only Cash On Delivery (COD) is currently supported. More payment methods may be added later. - -This project is tested with BrowserStack. - -## TODO - -- Implement UserRegistration.component.tsx in a registration page -- Add user dashboard with order history -- Add Cloudflare Turnstile on registration page -- Ensure email is real on registration page -- Add total to cart/checkout page -- Copy billing address to shipping address -- Hide products not in stock -- Add better SEO -``` - -## File: renovate.json -```json -{ - "extends": ["config:base"], - "ignorePresets": [":prHourlyLimit2", ":prConcurrentLimit20"], - "packageRules": [ - { - "rangeStrategy": "bump", - "matchDepTypes": [ - "dependencies", - "devDependencies", - "optionalDependencies", - "peerDependencies" - ] - }, - { - "matchUpdateTypes": ["minor", "pin", "digest"], - "automerge": true, - "matchDepTypes": [ - "dependencies", - "devDependencies", - "optionalDependencies", - "peerDependencies" - ] - }, - { - "matchUpdateTypes": ["patch", "lockFileMaintenance"], - "automerge": true, - "automergeType": "branch", - "matchDepTypes": [ - "dependencies", - "devDependencies", - "optionalDependencies", - "peerDependencies" - ] - } - ] -} -``` - -## File: SUGGESTIONS.md -```markdown -# Suggestions for Senior-Level Improvements - -## Testing Improvements -- **Unit Testing with Jest/RTL** - - Gain: Catch bugs early, ensure component behavior, easier refactoring - - Example: Test hooks like useProductFilters in isolation - -- **Visual Regression Testing** - - Gain: Catch unintended UI changes, ensure consistent design - - Example: Compare screenshots before/after changes to ProductCard - -- **Performance Testing** - - Gain: Monitor and maintain site speed, identify bottlenecks - - Example: Set Lighthouse score thresholds in CI - -## Error Handling -- **Error Boundaries** - - Gain: Graceful failure handling, better user experience - - Example: Fallback UI for failed product loads - -- **Error Tracking** - - Gain: Better debugging, understand user issues - - Example: Integration with error tracking service - -## Developer Experience -- **Storybook Integration** - - Gain: Better component documentation, easier UI development - - Example: Document all variants of ProductCard - -- **Stricter TypeScript** - - Gain: Catch more bugs at compile time, better maintainability - - Example: Enable strict mode, add proper generics - -## Performance -- **Code Splitting** - - Gain: Faster initial load, better resource utilization - - Example: Lazy load product filters on mobile - -- **Image Optimization** - - Gain: Faster page loads, better Core Web Vitals - - Example: Implement proper next/image strategy - -## Monitoring -- **Analytics** - - Gain: Understand user behavior, make data-driven improvements - - Example: Track filter usage, cart abandonment - -- **Performance Monitoring** - - Gain: Catch performance regressions, ensure good user experience - - Example: Monitor and alert on Core Web Vitals - -## Accessibility -- **Automated A11y Testing** - - Gain: Ensure consistent accessibility, catch regressions - - Example: Add axe-core to CI pipeline - -## Documentation -- **API Documentation** - - Gain: Easier onboarding, better maintainability - - Example: Document GraphQL schema usage - -Each suggestion focuses on improving code quality, maintainability, or user experience rather than adding new features. This is because: - -1. Core e-commerce features (login, dashboard) are already planned in TODO -2. Senior-level improvements often focus on non-functional requirements -3. These improvements demonstrate architectural thinking beyond feature development - -## Implementation Priority - -1. Testing Improvements - - Highest impact on code quality and maintainability - - Demonstrates professional development practices - - Makes future changes safer - -2. Error Handling - - Direct impact on user experience - - Shows consideration for edge cases - - Professional error management - -3. Developer Experience - - Makes codebase more maintainable - - Helps onboard other developers - - Shows understanding of team dynamics - -4. Performance & Monitoring - - Important for scalability - - Shows understanding of production concerns - - Data-driven improvements - -These improvements would elevate the project from a feature demonstration to a production-ready application with professional-grade infrastructure. -``` - -## File: tailwind.config.js -```javascript -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./src/components/**/*.tsx', './src/pages/**/*.tsx'], - theme: { - extend: { - backgroundImage: { - 'hero-background': "url('/images/hero.jpg')", - }, - }, - }, - plugins: [], -}; -``` - -## File: tsconfig.json -```json -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} -```