Migrate to Next

This commit is contained in:
2022-11-11 22:41:53 +00:00
parent 58e38dbedd
commit 4aaad4d704
352 changed files with 15583 additions and 21335 deletions

26
components/Button.js Normal file
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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%;
}
}

View 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>
);
}

View 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
View 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>;
}

View File

@@ -0,0 +1,3 @@
.block p {
margin: 0 0 var(--spacing-md);
}

54
components/Footer.js Normal file
View 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">
&copy;
{year} ABC Project Ltd
</Text>
</div>
</footer>
);
}

View 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);
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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%;
}

View 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>
);
}

View 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
View 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}
/>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View File

@@ -0,0 +1,9 @@
.block {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main {
flex: 1;
}

20
components/Link.js Normal file
View 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>
);
}

View 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
View 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
View 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
View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>
);
}

View 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
View 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>;
}

View 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
View 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";