Migrate to Next
This commit is contained in:
26
components/Button.js
Normal file
26
components/Button.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
export default function Button({ children, type, href }) {
|
||||
const isInternal = /^\/(?!\/)/.test(href);
|
||||
if (isInternal) {
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a className={styles.block}>{children}</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
if (type === "submit") {
|
||||
return (
|
||||
<button type="submit" className={styles.block}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a className={styles.block} href={href}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
18
components/Button.module.css
Normal file
18
components/Button.module.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.block {
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-dark);
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--color-dark);
|
||||
font-size: var(--font-size-md);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.block:visited {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 2px solid var(--color-dark);
|
||||
}
|
||||
22
components/Card.js
Normal file
22
components/Card.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import styles from "./Card.module.css";
|
||||
|
||||
export default function Card({ image, heading, children, href }) {
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a className={styles.block}>
|
||||
{image && (
|
||||
<span className={styles.media} aria-hidden="true">
|
||||
<Image src={image} layout="fill" objectFit="fill" />
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.content}>
|
||||
{heading && <span className={styles.head}>{heading}</span>}
|
||||
<span className={styles.body}>{children}</span>
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
49
components/Card.module.css
Normal file
49
components/Card.module.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.block {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1.334;
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 4px solid var(--color-black);
|
||||
}
|
||||
|
||||
.media {
|
||||
transition: transform 0.15s ease-in;
|
||||
}
|
||||
|
||||
.body {
|
||||
visibility: hidden;
|
||||
transition: visibility 1ms linear 0.15s;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) var(--spacing-xs);
|
||||
color: var(--color-white);
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
transform: translate3d(0, calc(100% - 58px), 0);
|
||||
transition: transform 0.15s ease-in, background-color 0.3s ease-in;
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.block:hover .content {
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.block:hover .media {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.block:hover .body {
|
||||
visibility: visible;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
89
components/CaseStudy.js
Normal file
89
components/CaseStudy.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import { Icon, IconWithText, Text } from ".";
|
||||
import {
|
||||
CheckSvg,
|
||||
ClockSvg,
|
||||
CalendarSvg,
|
||||
PoundSvg,
|
||||
TableListSvg,
|
||||
} from "../icons";
|
||||
import styles from "./CaseStudy.module.css";
|
||||
|
||||
export default function CaseStudy({ data }) {
|
||||
const { features, price, duration, date } = data;
|
||||
const formattedDate = new Date(date).toLocaleString("en-GB", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.features}>
|
||||
<IconWithText
|
||||
icon={
|
||||
<Icon>
|
||||
<TableListSvg />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
<Text weight="bold" size="lg" noMargin>
|
||||
Features:
|
||||
</Text>
|
||||
</IconWithText>
|
||||
<ul className={styles.featurelist}>
|
||||
{features.map((feature) => (
|
||||
<li key={feature}>
|
||||
<Icon size="lg" color="success">
|
||||
<CheckSvg />
|
||||
</Icon>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.rest}>
|
||||
<IconWithText
|
||||
icon={
|
||||
<Icon>
|
||||
<PoundSvg />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
<Text weight="bold" size="lg" noMargin>
|
||||
Total Cost:
|
||||
</Text>
|
||||
</IconWithText>
|
||||
<Text noMargin>{price}</Text>
|
||||
</div>
|
||||
<div className={styles.rest}>
|
||||
<IconWithText
|
||||
icon={
|
||||
<Icon>
|
||||
<ClockSvg />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
<Text weight="bold" size="lg" noMargin>
|
||||
Time taken:
|
||||
</Text>
|
||||
</IconWithText>
|
||||
<Text noMargin>{duration}</Text>
|
||||
</div>
|
||||
<div className={styles.rest}>
|
||||
<IconWithText
|
||||
icon={
|
||||
<Icon>
|
||||
<CalendarSvg />
|
||||
</Icon>
|
||||
}
|
||||
>
|
||||
<Text weight="bold" size="lg" noMargin>
|
||||
Date:
|
||||
</Text>
|
||||
</IconWithText>
|
||||
<Text noMargin>{formattedDate}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
components/CaseStudy.module.css
Normal file
48
components/CaseStudy.module.css
Normal file
@@ -0,0 +1,48 @@
|
||||
.block {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background-color: var(--color-dark-grey);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm) var(--spacing-lg);
|
||||
max-width: var(--page-max-width);
|
||||
margin-inline: auto;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.featurelist {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.featurelist li {
|
||||
display: inline-flex;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rest {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
@media (max-width: 40em) {
|
||||
.features {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
18
components/ContactButton.js
Normal file
18
components/ContactButton.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import classnames from "classnames";
|
||||
import styles from "./ContactButton.module.css";
|
||||
|
||||
export default function ContactButton({ children, href, size }) {
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<a
|
||||
className={classnames(styles.block, {
|
||||
[styles.sm]: size === "sm",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
13
components/ContactButton.module.css
Normal file
13
components/ContactButton.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
color: var(--color-dark);
|
||||
font-size: var(--font-size-xl);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sm {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
6
components/Content.js
Normal file
6
components/Content.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import styles from "./Content.module.css";
|
||||
|
||||
export default function Content({ children }) {
|
||||
return <div className={styles.block}>{children}</div>;
|
||||
}
|
||||
3
components/Content.module.css
Normal file
3
components/Content.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.block p {
|
||||
margin: 0 0 var(--spacing-md);
|
||||
}
|
||||
54
components/Footer.js
Normal file
54
components/Footer.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { ContactButton, Grid, Icon, Text } from ".";
|
||||
import { FacebookSvg, EnvelopeSvg, MobileSvg, PhoneSvg } from "../icons";
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
export default function Footer() {
|
||||
const year = new Date().getFullYear();
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.content}>
|
||||
<Grid mdFillFit>
|
||||
<div className={styles.buttons}>
|
||||
<ContactButton href="tel:+442071013958" size="sm">
|
||||
<Icon size="lg">
|
||||
<PhoneSvg />
|
||||
</Icon>
|
||||
02071013958
|
||||
</ContactButton>
|
||||
<ContactButton href="tel:+447378839358" size="sm">
|
||||
<Icon size="lg">
|
||||
<MobileSvg />
|
||||
</Icon>
|
||||
07378839358
|
||||
</ContactButton>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<ContactButton
|
||||
href="mailto:quotations@abcprojectdesign.com"
|
||||
size="sm"
|
||||
>
|
||||
<Icon size="lg">
|
||||
<EnvelopeSvg />
|
||||
</Icon>
|
||||
quotations@abcprojectdesign.com
|
||||
</ContactButton>
|
||||
<ContactButton
|
||||
href="https://www.facebook.com/people/ABC-Project-Design/100088268944294/"
|
||||
size="sm"
|
||||
>
|
||||
<Icon size="lg">
|
||||
<FacebookSvg />
|
||||
</Icon>
|
||||
Facebook
|
||||
</ContactButton>
|
||||
</div>
|
||||
</Grid>
|
||||
<Text size="xs">
|
||||
©
|
||||
{year} ABC Project Ltd
|
||||
</Text>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
20
components/Footer.module.css
Normal file
20
components/Footer.module.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.footer {
|
||||
border-top: 1px solid #eaeaea;
|
||||
padding: var(--spacing-lg) var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
max-width: var(--page-max-width);
|
||||
margin-inline: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
80
components/GalleryOverlay.js
Normal file
80
components/GalleryOverlay.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { Icon } from ".";
|
||||
import styles from "./GalleryOverlay.module.css";
|
||||
import { ArrowSvg, XMarkSvg } from "../icons";
|
||||
|
||||
export default function GalleryOverlay({
|
||||
onClose,
|
||||
onPrev,
|
||||
onNext,
|
||||
image,
|
||||
desc,
|
||||
}) {
|
||||
const headingRef = useRef();
|
||||
const firstRef = useRef();
|
||||
const lastRef = useRef();
|
||||
useEffect(() => {
|
||||
headingRef.current.focus();
|
||||
}, []);
|
||||
const handleKeyUp = (e) => {
|
||||
switch (e.keyCode) {
|
||||
case 27:
|
||||
onClose();
|
||||
break;
|
||||
case 37:
|
||||
onPrev();
|
||||
break;
|
||||
case 39:
|
||||
onNext();
|
||||
break;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={styles.block} onKeyUp={handleKeyUp}>
|
||||
<button
|
||||
className={styles.hidden}
|
||||
onFocus={() => lastRef.current.focus()}
|
||||
/>
|
||||
<h1 className={styles.hidden} tabIndex="-1" ref={headingRef}>
|
||||
{desc}
|
||||
</h1>
|
||||
<div className={styles.content}>
|
||||
<button
|
||||
className={styles.prev}
|
||||
aria-label="Previous image"
|
||||
onClick={onPrev}
|
||||
ref={firstRef}
|
||||
>
|
||||
<Icon size="lg" rotate="270">
|
||||
<ArrowSvg />
|
||||
</Icon>
|
||||
</button>
|
||||
<Image src={image} alt={desc} layout="fill" objectFit="contain" />
|
||||
<button
|
||||
className={styles.next}
|
||||
aria-label="Next image"
|
||||
onClick={onNext}
|
||||
>
|
||||
<Icon size="lg" rotate="90">
|
||||
<ArrowSvg />
|
||||
</Icon>
|
||||
</button>
|
||||
<button
|
||||
className={styles.close}
|
||||
aria-label="Close"
|
||||
onClick={onClose}
|
||||
ref={lastRef}
|
||||
>
|
||||
<Icon size="lg">
|
||||
<XMarkSvg />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={styles.hidden}
|
||||
onFocus={() => firstRef.current.focus()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
components/GalleryOverlay.module.css
Normal file
64
components/GalleryOverlay.module.css
Normal file
@@ -0,0 +1,64 @@
|
||||
.block {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
background-color: rgb(0, 0, 0, 0.75);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
background-color: var(--color-dark-grey);
|
||||
}
|
||||
|
||||
.close,
|
||||
.next,
|
||||
.prev {
|
||||
position: absolute;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.close:focus,
|
||||
.next:focus,
|
||||
.prev:focus {
|
||||
outline: 2px solid var(--color-white);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.close {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.next,
|
||||
.prev {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.prev {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.next {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
clip: rect(0 0 0 0);
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
11
components/GalleryThumb.js
Normal file
11
components/GalleryThumb.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import styles from "./GalleryThumb.module.css";
|
||||
|
||||
export default function GalleryThumb({ image, label, onClick }) {
|
||||
return (
|
||||
<button className={styles.block} onClick={onClick} aria-label={label}>
|
||||
<Image src={image} layout="fill" objectFit="fill" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
11
components/GalleryThumb.module.css
Normal file
11
components/GalleryThumb.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.block {
|
||||
position: relative;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 2px solid var(--color-dark);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
21
components/Grid.js
Normal file
21
components/Grid.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import styles from "./Grid.module.css";
|
||||
|
||||
export default function Grid({ d, sm, md, mdFillFit, children }) {
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.block, {
|
||||
[styles.d2]: d === 2,
|
||||
[styles.sm2]: sm === 2,
|
||||
[styles.sm3]: sm === 3,
|
||||
[styles.md2]: md === 2,
|
||||
[styles.md3]: md === 3,
|
||||
[styles.md4]: md === 4,
|
||||
[styles.mdfillfit]: mdFillFit,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
components/Grid.module.css
Normal file
70
components/Grid.module.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.block {
|
||||
--grid-cols: 1;
|
||||
display: grid;
|
||||
gap: var(--spacing-lg) var(--spacing-md);
|
||||
grid-template-columns: repeat(var(--grid-cols), minmax(10px, 1fr));
|
||||
}
|
||||
|
||||
.d2 {
|
||||
--grid-cols: 2;
|
||||
}
|
||||
|
||||
@media (min-width: 30em) {
|
||||
.sm2 {
|
||||
--grid-cols: 2;
|
||||
}
|
||||
.sm3 {
|
||||
--grid-cols: 3;
|
||||
}
|
||||
}
|
||||
@media (min-width: 40em) {
|
||||
.md2 {
|
||||
--grid-cols: 2;
|
||||
}
|
||||
.md3 {
|
||||
--grid-cols: 3;
|
||||
}
|
||||
.md4 {
|
||||
--grid-cols: 4;
|
||||
}
|
||||
|
||||
.mdfillfit {
|
||||
grid-template-columns: 1fr max-content;
|
||||
}
|
||||
}
|
||||
/*
|
||||
${(props) =>
|
||||
props.columns &&
|
||||
props.columns.map((column, i) =>
|
||||
i === 0 && column > 1
|
||||
? css`
|
||||
grid-template-columns: repeat(${column}, minmax(10px, 1fr));
|
||||
`
|
||||
: css`
|
||||
@media (min-width: ${props.breakpoints[i - 1]}) {
|
||||
grid-template-columns: repeat(${column}, minmax(10px, 1fr));
|
||||
${
|
||||
i === 1 &&
|
||||
css`
|
||||
gap: var(--spacing-y-lg) var(--spacing-x-lg);
|
||||
`
|
||||
}
|
||||
`
|
||||
)}
|
||||
|
||||
${(props) =>
|
||||
props.verticalSeparator &&
|
||||
css`
|
||||
@media (max-width: ${props.verticalSeparator}) {
|
||||
> * {
|
||||
padding-bottom: var(--spacing-y-lg);
|
||||
}
|
||||
|
||||
> :not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--color-mid-grey);
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
*/
|
||||
30
components/Header.js
Normal file
30
components/Header.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import classnames from "classnames";
|
||||
import { Heading } from ".";
|
||||
import styles from "./Header.module.css";
|
||||
|
||||
export default function Header({
|
||||
image,
|
||||
title,
|
||||
light,
|
||||
map,
|
||||
position,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
{image && <Image src={image} layout="fill" objectFit="cover" />}
|
||||
{map && <div className={styles.map}>{map}</div>}
|
||||
<div
|
||||
className={classnames(styles.content, {
|
||||
[styles.br]: position === "br",
|
||||
[styles.light]: light,
|
||||
})}
|
||||
>
|
||||
{title && <Heading size="xxl">{title}</Heading>}
|
||||
{!title && children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
components/Header.module.css
Normal file
68
components/Header.module.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.block {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 50vh;
|
||||
min-height: 280px;
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: var(--page-max-width);
|
||||
padding: var(--spacing-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.light {
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.br {
|
||||
top: auto;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
transform: none;
|
||||
}
|
||||
/*
|
||||
const Content = styled.div`
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
props.posY !== "bottom" &&
|
||||
css`
|
||||
top: ${props.posY === "top" ? 0 : "50%"};
|
||||
`}
|
||||
${(props) =>
|
||||
props.posY === "bottom" &&
|
||||
css`
|
||||
bottom: 0;
|
||||
`}
|
||||
${(props) =>
|
||||
props.posX !== "right" &&
|
||||
css`
|
||||
left: ${props.posX === "left" ? 0 : "50%"};
|
||||
`}
|
||||
${(props) =>
|
||||
props.posX === "right" &&
|
||||
css`
|
||||
right: 0;
|
||||
`}
|
||||
transform: translate(
|
||||
${(props) => (props.posX === "middle" ? "-50%" : 0)},
|
||||
${(props) => (props.posY === "middle" ? "-50%" : 0)}
|
||||
);
|
||||
max-width: var(--page-max-width);
|
||||
padding: var(--spacing-y-sm) var(--spacing-x-sm);
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Map = styled.div``;
|
||||
*/
|
||||
26
components/HeaderLogo.js
Normal file
26
components/HeaderLogo.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import { Heading, Text } from ".";
|
||||
import styles from "./HeaderLogo.module.css";
|
||||
import { LogoArrowsSvg } from "../icons";
|
||||
|
||||
export default function HeaderLogo({ transition }) {
|
||||
const [visible, setVisible] = useState(!transition);
|
||||
useEffect(() => transition && setVisible(true), [transition]);
|
||||
return (
|
||||
<div className={classnames(styles.block, { [styles.isvisible]: visible })}>
|
||||
<div className={styles.top}>
|
||||
<LogoArrowsSvg />
|
||||
</div>
|
||||
<div className={styles.middle}>
|
||||
<Heading>ABC Project Design Ltd</Heading>
|
||||
<Text size="lg" noMargin>
|
||||
Easy as ABC
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.bottom}>
|
||||
<LogoArrowsSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
components/HeaderLogo.module.css
Normal file
57
components/HeaderLogo.module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.block {
|
||||
--header-logo-filter: grayscale(100%);
|
||||
--header-logo-opacity: 0;
|
||||
--header-logo-top-transform: translate3d(0, 50%, 0);
|
||||
--header-logo-bottom-transform: translate3d(0, -50%, 0) rotate(180deg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top {
|
||||
width: 120px;
|
||||
height: 60px;
|
||||
filter: var(--header-logo-filter);
|
||||
opacity: var(--header-logo-opacity);
|
||||
transform: var(--header-logo-top-transform);
|
||||
transition: opacity 0.5s ease-in, filter 0.5s ease-in 0.5s,
|
||||
transform 0.5s ease-in 0.5s;
|
||||
}
|
||||
|
||||
.top path:first-of-type {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
|
||||
.top path:last-of-type {
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
||||
.middle {
|
||||
opacity: var(--header-logo-opacity);
|
||||
transition: opacity 0.5s ease-in 0.75s;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
width: 120px;
|
||||
height: 60px;
|
||||
filter: var(--header-logo-filter);
|
||||
opacity: var(--header-logo-opacity);
|
||||
transform: var(--header-logo-bottom-transform);
|
||||
transition: opacity 0.5s ease-in, filter 0.5s ease-in 0.5s,
|
||||
transform 0.5s ease-in 0.5s;
|
||||
}
|
||||
|
||||
.bottom path:first-of-type {
|
||||
fill: var(--color-dark);
|
||||
}
|
||||
|
||||
.bottom path:last-of-type {
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
||||
.isvisible {
|
||||
--header-logo-filter: none;
|
||||
--header-logo-opacity: 1;
|
||||
--header-logo-top-transform: translate3d(0, 0, 0);
|
||||
--header-logo-bottom-transform: translate3d(0, 0, 0) rotate(180deg);
|
||||
}
|
||||
17
components/Heading.js
Normal file
17
components/Heading.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import styles from "./Heading.module.css";
|
||||
|
||||
export default function Heading({ el: El = "h1", size, children }) {
|
||||
return (
|
||||
<El
|
||||
className={classnames(styles.block, {
|
||||
[styles.md]: size === "md",
|
||||
[styles.lg]: size === "lg",
|
||||
[styles.xl]: size === "xl",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</El>
|
||||
);
|
||||
}
|
||||
38
components/Heading.module.css
Normal file
38
components/Heading.module.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.block {
|
||||
margin: 0 0 var(---spacing-sm);
|
||||
font-size: var(--font-size-xxl);
|
||||
}
|
||||
|
||||
.xl {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.lg {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.md {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
/*
|
||||
text-shadow: 0 0 ${(props) => props.shadowSize}
|
||||
${(props) => props.shadowColor};
|
||||
${(props) =>
|
||||
props.color &&
|
||||
css`
|
||||
color: var(--color-${props.color});
|
||||
`}
|
||||
${(props) =>
|
||||
props.noWrap &&
|
||||
css`
|
||||
white-space: nowrap;
|
||||
`}
|
||||
${(props) =>
|
||||
props.align &&
|
||||
css`
|
||||
text-align: ${props.align};
|
||||
`}
|
||||
`;
|
||||
|
||||
*/
|
||||
23
components/Icon.js
Normal file
23
components/Icon.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import styles from "./Icon.module.css";
|
||||
|
||||
export default function Icon({ children, size, rotate, color }) {
|
||||
return (
|
||||
<span
|
||||
className={classnames(styles.block, {
|
||||
[styles.sm]: size === "sm",
|
||||
[styles.lg]: size === "lg",
|
||||
[styles.xl]: size === "xl",
|
||||
[styles.primary]: color === "primary",
|
||||
[styles.dark]: color === "dark",
|
||||
[styles.success]: color === "success",
|
||||
[styles.rotate90]: rotate === "90",
|
||||
[styles.rotate180]: rotate === "180",
|
||||
[styles.rotate270]: rotate === "270",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
49
components/Icon.module.css
Normal file
49
components/Icon.module.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.block {
|
||||
display: inline-flex;
|
||||
width: var(--icon-size-md);
|
||||
height: var(--icon-side-md);
|
||||
}
|
||||
|
||||
.sm {
|
||||
width: var(--icon-size-sm);
|
||||
height: var(--icon-size-sm);
|
||||
}
|
||||
|
||||
.lg {
|
||||
width: var(--icon-size-lg);
|
||||
height: var(--icon-size-lg);
|
||||
}
|
||||
|
||||
.xl {
|
||||
width: var(--icon-size-xl);
|
||||
height: var(--icon-size-xl);
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.rotate90 {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.rotate180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.rotate270 {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
.block svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
11
components/IconWithText.js
Normal file
11
components/IconWithText.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import styles from "./IconWithText.module.css";
|
||||
|
||||
export default function IconWithText({ icon, children }) {
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<div className={styles.icon}>{icon}</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
components/IconWithText.module.css
Normal file
10
components/IconWithText.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.block {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
16
components/InputField.js
Normal file
16
components/InputField.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import styles from "./InputField.module.css";
|
||||
|
||||
export default function InputField({ id, name, value, onChange, textArea }) {
|
||||
const El = textArea ? "textarea" : "input";
|
||||
return (
|
||||
<El
|
||||
className={styles.block}
|
||||
type="text"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
10
components/InputField.module.css
Normal file
10
components/InputField.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.block {
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
border: 1px solid var(--color-dark);
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 2px solid var(--color-dark);
|
||||
}
|
||||
19
components/Interrupt.js
Normal file
19
components/Interrupt.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import styles from "./Interrupt.module.css";
|
||||
|
||||
export default function Interrupt() {
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<span className={styles.spinner}>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
117
components/Interrupt.module.css
Normal file
117
components/Interrupt.module.css
Normal file
@@ -0,0 +1,117 @@
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.spinner span {
|
||||
display: inline-block;
|
||||
animation: rotate 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
transform-origin: 40px 40px;
|
||||
}
|
||||
|
||||
.spinner span:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-dark);
|
||||
margin: -4px 0 0 -4px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(1) {
|
||||
animation-delay: -0.036s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(1):after {
|
||||
top: 63px;
|
||||
left: 63px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(2) {
|
||||
animation-delay: -0.072s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(2):after {
|
||||
top: 68px;
|
||||
left: 56px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(3) {
|
||||
animation-delay: -0.108s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(3):after {
|
||||
top: 71px;
|
||||
left: 48px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(4) {
|
||||
animation-delay: -0.144s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(4):after {
|
||||
top: 72px;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(5) {
|
||||
animation-delay: -0.18s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(5):after {
|
||||
top: 71px;
|
||||
left: 32px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(6) {
|
||||
animation-delay: -0.216s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(6):after {
|
||||
top: 68px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(7) {
|
||||
animation-delay: -0.252s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(7):after {
|
||||
top: 63px;
|
||||
left: 17px;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(8) {
|
||||
animation-delay: -0.288s;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(8):after {
|
||||
top: 56px;
|
||||
left: 12px;
|
||||
}
|
||||
10
components/Label.js
Normal file
10
components/Label.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import styles from "./Label.module.css";
|
||||
|
||||
export default function Label({ id, children }) {
|
||||
return (
|
||||
<label className={styles.block} htmlFor={id}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
5
components/Label.module.css
Normal file
5
components/Label.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.block {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
19
components/Layout.js
Normal file
19
components/Layout.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
import { Footer, Nav } from ".";
|
||||
import styles from "./Layout.module.css";
|
||||
|
||||
export default function Layout({ title, children }) {
|
||||
const pageTitle = `${title ? `${title} : ` : ""}ABC Project Design`;
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
components/Layout.module.css
Normal file
9
components/Layout.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
}
|
||||
20
components/Link.js
Normal file
20
components/Link.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import NextLink from "next/link";
|
||||
import styles from "./Link.module.css";
|
||||
|
||||
export default function Link({ href, children, ...rest }) {
|
||||
if (/^\/(?!\/)/.test(href)) {
|
||||
return (
|
||||
<NextLink href={href} passHref>
|
||||
<a className={styles.block} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
</NextLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a className={styles.block} href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
10
components/Link.module.css
Normal file
10
components/Link.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.block {
|
||||
color: var(--color-dark);
|
||||
text-decoration-color: var(--color-primary);
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 2px solid var(--color-dark);
|
||||
}
|
||||
96
components/Map.js
Normal file
96
components/Map.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import {
|
||||
GoogleMap,
|
||||
Marker,
|
||||
Polyline,
|
||||
useJsApiLoader,
|
||||
} from "@react-google-maps/api";
|
||||
|
||||
const triangleCoords = [
|
||||
{ lat: 52.253, lng: 0.109 }, // Cambridge
|
||||
{ lat: 52.095, lng: 1.144 }, // Ipswich
|
||||
{ lat: 51.975, lng: 1.379 }, // Felixtowe
|
||||
{ lat: 51.782, lng: 1.151 }, // Clacton
|
||||
{ lat: 51.755, lng: 0.975 }, // Blackwater
|
||||
{ lat: 51.612, lng: 0.987 }, // Courtsend
|
||||
{ lat: 51.453, lng: 0.733 }, // Grain
|
||||
{ lat: 51.228, lng: 0.554 }, // Maidstone
|
||||
{ lat: 51.258, lng: -0.087 }, // Caterham
|
||||
{ lat: 51.436, lng: -0.361 }, // Twickenham
|
||||
{ lat: 51.611, lng: -0.45 }, // Northwood
|
||||
{ lat: 51.737, lng: -0.399 }, // St Albans
|
||||
{ lat: 51.992, lng: -0.281 }, // Letchworth
|
||||
{ lat: 52.253, lng: 0.109 }, // Cambridge
|
||||
];
|
||||
|
||||
const getContainerStyle = ({ fullHeight }) => ({
|
||||
width: "100%",
|
||||
height: fullHeight ? "100%" : "50vh",
|
||||
maxHeight: "500px",
|
||||
});
|
||||
|
||||
const center = {
|
||||
lat: 51.765,
|
||||
lng: 0.022,
|
||||
};
|
||||
|
||||
const marker = {
|
||||
lat: 51.536,
|
||||
lng: 0.176,
|
||||
};
|
||||
|
||||
export default function CustomMap({
|
||||
fullHeight,
|
||||
showMarker,
|
||||
showRegion,
|
||||
zoom = 7,
|
||||
apikey,
|
||||
}) {
|
||||
const { isLoaded } = useJsApiLoader({
|
||||
id: "google-map-script",
|
||||
googleMapsApiKey: apikey,
|
||||
});
|
||||
|
||||
const [map, setMap] = useState(null);
|
||||
|
||||
const onLoad = useCallback((map) => {
|
||||
// This is just an example of getting and using the map instance!!! don't just blindly copy!
|
||||
const bounds = new window.google.maps.LatLngBounds(center);
|
||||
map.fitBounds(bounds);
|
||||
|
||||
setMap(map);
|
||||
}, []);
|
||||
|
||||
const onUnmount = useCallback((map) => {
|
||||
setMap(null);
|
||||
}, []);
|
||||
|
||||
return isLoaded ? (
|
||||
<GoogleMap
|
||||
mapContainerStyle={getContainerStyle({ fullHeight })}
|
||||
center={showMarker ? marker : center}
|
||||
zoom={zoom}
|
||||
options={{
|
||||
fullscreenControl: false,
|
||||
gestureHandling: "none",
|
||||
zoomControl: false,
|
||||
}}
|
||||
onUnmount={onUnmount}
|
||||
>
|
||||
{showRegion && (
|
||||
<Polyline
|
||||
path={triangleCoords}
|
||||
strokeColor="var(--color-primary)"
|
||||
fillColor="#ff0000"
|
||||
strokeOpacity={0.8}
|
||||
strokeWeight={2}
|
||||
/>
|
||||
)}
|
||||
{showMarker && (
|
||||
<Marker title="ABC Project Design Ltd" position={marker} />
|
||||
)}
|
||||
</GoogleMap>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
94
components/Nav.js
Normal file
94
components/Nav.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import Link from "next/link";
|
||||
import { Icon, IconWithText } from ".";
|
||||
import styles from "./Nav.module.css";
|
||||
import { FacebookSvg, LogoArrowsSvg } from "../icons";
|
||||
|
||||
export default function Nav() {
|
||||
const [expanded, setExpanded] = useState();
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.block, { [styles.isexpanded]: expanded })}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.home}>
|
||||
<Link href="/" passHref>
|
||||
<a className={styles.homelink} aria-label="Home">
|
||||
<span className={styles.homeicontop}>
|
||||
<LogoArrowsSvg />
|
||||
</span>
|
||||
<span className={styles.homeiconbottom}>
|
||||
<LogoArrowsSvg />
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.menu}>
|
||||
<button
|
||||
className={styles.menubutton}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
aria-label={expanded ? "Close menu" : "Open menu"}
|
||||
>
|
||||
<span className={styles.topbar} />
|
||||
<span className={styles.middlebar} />
|
||||
<span className={styles.bottombar} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.links}>
|
||||
<Link
|
||||
href="https://www.facebook.com/people/ABC-Project-Design/100088268944294/"
|
||||
passHref
|
||||
>
|
||||
<a className={styles.link} aria-label="Visit our Facebook page">
|
||||
<Icon size="lg">
|
||||
<FacebookSvg />
|
||||
</Icon>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className={styles.nav}>
|
||||
<ul className={styles.list}>
|
||||
<li className={classnames(styles.listitem, styles.transform)}>
|
||||
<Link href="/" passHref>
|
||||
<a className={styles.link}>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={classnames(styles.listitem, styles.transform)}>
|
||||
<Link href="/services" passHref>
|
||||
<a className={styles.link}>Services</a>
|
||||
</Link>
|
||||
<ul className={styles.sublist}>
|
||||
<li className={styles.listitem}>
|
||||
<Link href="/services/bathrooms" passHref>
|
||||
<a className={styles.link}>Bathrooms</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.listitem}>
|
||||
<Link href="/services/tiling" passHref>
|
||||
<a className={styles.link}>Tiling</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className={classnames(styles.listitem, styles.transform)}>
|
||||
<Link href="/area-covered" passHref>
|
||||
<a className={styles.link}>Area covered</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={classnames(styles.listitem, styles.transform)}>
|
||||
<Link href="/about" passHref>
|
||||
<a className={styles.link}>About</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={classnames(styles.listitem, styles.transform)}>
|
||||
<Link href="/contact" passHref>
|
||||
<a className={styles.link}>Contact</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
262
components/Nav.module.css
Normal file
262
components/Nav.module.css
Normal file
@@ -0,0 +1,262 @@
|
||||
.block {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(2px);
|
||||
transform: translate3d(0, calc(-100% + 3rem), 0);
|
||||
transition: transform 0.3s ease-in-out 0s,
|
||||
background-color 0.3s ease-in-out 0s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.isexpanded {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"nav nav nav"
|
||||
"home links menu";
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
grid-template-rows: 1fr max-content;
|
||||
height: 100%;
|
||||
max-width: var(--page-max-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.isexpanded .container {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.home {
|
||||
display: flex;
|
||||
grid-area: home;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-area: menu;
|
||||
height: 3rem;
|
||||
margin-inline: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
grid-area: links;
|
||||
}
|
||||
|
||||
.nav {
|
||||
grid-area: nav;
|
||||
}
|
||||
|
||||
.homelink {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding-inline: var(--spacing-md);
|
||||
}
|
||||
|
||||
.homelink:focus {
|
||||
outline: 2px solid var(--color-white);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.homeicontop,
|
||||
.homeiconbottom {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.homeicontop path:first-of-type {
|
||||
fill: var(--color-primary);
|
||||
}
|
||||
|
||||
.homeicontop path:last-of-type {
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
||||
.homeiconbottom {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.homeiconbottom path:first-of-type {
|
||||
fill: var(--color-dark);
|
||||
}
|
||||
|
||||
.homeiconbottom path:last-of-type {
|
||||
fill: var(--color-secondary);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.listitem {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listitem:focus-within .sublist,
|
||||
.listitem:hover .sublist {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sublist {
|
||||
margin: 0 0 0 var(--spacing-sm);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
color: var(--color-white);
|
||||
text-decoration: none;
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.link:focus {
|
||||
outline: 2px solid var(--color-white);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.menubutton {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: 24px;
|
||||
background-color: transparent;
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menubutton:focus {
|
||||
outline: 2px solid var(--color-white);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.topbar,
|
||||
.middlebar,
|
||||
.bottombar {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
left: 4px;
|
||||
border-top: 3px solid var(--color-white);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
top: 5px;
|
||||
transition: 0.55s 0.5s;
|
||||
}
|
||||
|
||||
.isexpanded .topbar {
|
||||
transform: translate3d(0, 10px, 0) rotate(50deg);
|
||||
}
|
||||
|
||||
.middlebar {
|
||||
top: 15px;
|
||||
transition: opacity 1s ease-in;
|
||||
}
|
||||
|
||||
.isexpanded .middlebar {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bottombar {
|
||||
top: 25px;
|
||||
transition: 0.55s 0.5s;
|
||||
}
|
||||
|
||||
.isexpanded .bottombar {
|
||||
transform: translate3d(0, -10px, 0) rotate(-410deg);
|
||||
}
|
||||
|
||||
@media (max-width: 29.999em) {
|
||||
.transform {
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.transform:nth-child(1) {
|
||||
transition-delay: 0.15s;
|
||||
}
|
||||
|
||||
.transform:nth-child(2) {
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
|
||||
.transform:nth-child(3) {
|
||||
transition-delay: 0.35s;
|
||||
}
|
||||
|
||||
.transform:nth-child(4) {
|
||||
transition-delay: 0.45s;
|
||||
}
|
||||
|
||||
.transform:nth-child(5) {
|
||||
transition-delay: 0.55s;
|
||||
}
|
||||
|
||||
.isexpanded .transform {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 30em) {
|
||||
.block {
|
||||
position: absolute;
|
||||
bottom: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
grid-template-areas: "home nav links";
|
||||
grid-template-columns: max-content 1fr max-content;
|
||||
}
|
||||
|
||||
.menu {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.menubutton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.listitem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sublist {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
transform: translateY(100%);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
32
components/Review.js
Normal file
32
components/Review.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Heading, Icon } from ".";
|
||||
import { CheckatradeLogoSvg, QuoteLeftSvg, QuoteRightSvg } from "../icons";
|
||||
import styles from "./Review.module.css";
|
||||
|
||||
export default function Review({ type, title, author, date, children }) {
|
||||
return (
|
||||
<div className={styles.block}>
|
||||
<div className={styles.head}>
|
||||
<Icon size="xl">
|
||||
<CheckatradeLogoSvg />
|
||||
</Icon>
|
||||
<Heading el="h3" size="lg">
|
||||
{title}
|
||||
</Heading>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.prefix}>
|
||||
<Icon size="lg">
|
||||
<QuoteLeftSvg />
|
||||
</Icon>
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.suffix}>
|
||||
<Icon size="lg">
|
||||
<QuoteRightSvg />
|
||||
</Icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
components/Review.module.css
Normal file
40
components/Review.module.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
text-align: center;
|
||||
margin-inline: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-inline: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
position: absolute;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translateY(-66%);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translateY(66%);
|
||||
}
|
||||
23
components/Section.js
Normal file
23
components/Section.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import { Heading } from ".";
|
||||
import styles from "./Section.module.css";
|
||||
|
||||
export default function Section({ variant, children, title }) {
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.block, {
|
||||
[styles.secondary]: variant === "secondary",
|
||||
})}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
{title && (
|
||||
<div className={styles.head}>
|
||||
<Heading el="h2">{title}</Heading>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
components/Section.module.css
Normal file
21
components/Section.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.block {
|
||||
padding: var(--spacing-xl) var(--spacing-sm);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--color-light);
|
||||
}
|
||||
|
||||
.transparent {
|
||||
background-color: var(--color-semi-transparent);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: var(--page-max-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.head {
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
14
components/Select.js
Normal file
14
components/Select.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import styles from "./Select.module.css";
|
||||
|
||||
export default function Select({ id, name, value, options = [] }) {
|
||||
return (
|
||||
<select className={styles.block} id={id} name={name} value={value}>
|
||||
{options.map((opt) => (
|
||||
<option value={opt.value} key={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
11
components/Select.module.css
Normal file
11
components/Select.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.block {
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background-color: var(--color-white);
|
||||
border: 1px solid var(--color-dark);
|
||||
}
|
||||
|
||||
.block:focus {
|
||||
outline: 2px solid var(--color-dark);
|
||||
}
|
||||
31
components/Text.js
Normal file
31
components/Text.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import styles from "./Text.module.css";
|
||||
|
||||
export default function Text({
|
||||
children,
|
||||
alignment,
|
||||
size,
|
||||
weight,
|
||||
color,
|
||||
noMargin,
|
||||
}) {
|
||||
return (
|
||||
<p
|
||||
className={classnames(styles.block, {
|
||||
[styles.center]: alignment === "center",
|
||||
[styles.right]: alignment === "right",
|
||||
[styles.sm]: size === "sm",
|
||||
[styles.md]: size === "md",
|
||||
[styles.lg]: size === "lg",
|
||||
[styles.xl]: size === "xl",
|
||||
[styles.bold]: weight === "bold",
|
||||
[styles.nomargin]: noMargin,
|
||||
[styles.primary]: color === "primary",
|
||||
[styles.dark]: color === "dark",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
50
components/Text.module.css
Normal file
50
components/Text.module.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.block {
|
||||
margin: 0 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.xs {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.sm {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.md {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.lg {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.xl {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.nomargin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: var(--color-dark);
|
||||
}
|
||||
.block em {
|
||||
font-weight: 400;
|
||||
}
|
||||
6
components/Tile.js
Normal file
6
components/Tile.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import styles from "./Tile.module.css";
|
||||
|
||||
export default function Tile({ children }) {
|
||||
return <div className={styles.block}>{children}</div>;
|
||||
}
|
||||
8
components/Tile.module.css
Normal file
8
components/Tile.module.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
text-align: center;
|
||||
color: var(--color-mid-grey);
|
||||
align-items: center;
|
||||
}
|
||||
26
components/index.js
Normal file
26
components/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export { default as Button } from "./Button";
|
||||
export { default as Card } from "./Card";
|
||||
export { default as CaseStudy } from "./CaseStudy";
|
||||
export { default as ContactButton } from "./ContactButton";
|
||||
export { default as Content } from "./Content";
|
||||
export { default as Footer } from "./Footer";
|
||||
export { default as GalleryOverlay } from "./GalleryOverlay";
|
||||
export { default as GalleryThumb } from "./GalleryThumb";
|
||||
export { default as Grid } from "./Grid";
|
||||
export { default as Header } from "./Header";
|
||||
export { default as HeaderLogo } from "./HeaderLogo";
|
||||
export { default as Heading } from "./Heading";
|
||||
export { default as Icon } from "./Icon";
|
||||
export { default as IconWithText } from "./IconWithText";
|
||||
export { default as InputField } from "./InputField";
|
||||
export { default as Interrupt } from "./Interrupt";
|
||||
export { default as Label } from "./Label";
|
||||
export { default as Layout } from "./Layout";
|
||||
export { default as Link } from "./Link";
|
||||
export { default as Map } from "./Map";
|
||||
export { default as Nav } from "./Nav";
|
||||
export { default as Review } from "./Review";
|
||||
export { default as Section } from "./Section";
|
||||
export { default as Select } from "./Select";
|
||||
export { default as Text } from "./Text";
|
||||
export { default as Tile } from "./Tile";
|
||||
Reference in New Issue
Block a user