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

View File

@@ -1,19 +0,0 @@
module.exports = {
globals: {
__PATH_PREFIX__: true
},
env: {
browser: true,
es2021: true
},
extends: "react-app",
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 12,
sourceType: "module"
},
plugins: ["react"],
rules: {}
};

36
.gitignore vendored
View File

@@ -1,3 +1,33 @@
.cache # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules
public # dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# Local Netlify folder
.netlify

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

4
.lintstagedrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"**/*.css": ["stylelint --fix", "prettier --write"],
"**/*.js": ["prettier --write"]
}

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
.next

6
.stylelintrc Normal file
View File

@@ -0,0 +1,6 @@
{
"plugins": ["stylelint-prettier"],
"rules": {
"prettier/prettier": true
}
}

27
case-studies/101.md Normal file
View File

@@ -0,0 +1,27 @@
---
title: Modern Bathroom
location: Maldon, Essex
date: '2020-12-01'
features: [Damp proofing, Tiling, Alcove lighting]
price: £5,900
duration: 12 days
image: 0
images: 5
---
This medium sized bathroom in Maldon, Essex came with very few design problems.
The old bathroom was completely stripped out to a shell. The bathroom showed
small signs of damp which was easily rectified with a waterproof membrane
inserted within the shower walls.
The client decided to replace the old bath with a large walk-in shower. Client
chose to have a false ceiling installed with spot lights and an alcove with
electrics and lighting was fitted to give the bathroom a warm feeling.
We stripped out the old bathroom and disposed of all rubbish. We tiled, plastered
and painted the floor and walls, installed the new bathroom with tiling,
plastering, painting and all electrical works.
Total price for labour and building materials: £5,900.
(This does not include bathroom suite or tiles)

24
case-studies/102.md Normal file
View File

@@ -0,0 +1,24 @@
---
title: Compact Bathroom
location: Hackney, London
date: '2020-11-01'
features: [Damp proofing, Recessed shower mixer]
price: £5,000
duration: 10 days
image: 0
images: 3
---
Very small bathroom in a block of flats In Hackney, E9.
Existing bathroom was extremely old. We stripped the bathroom back to the shell
and addressed damp issues.
Client went with our suggestion of using two different wall tiles.
Shower mixer was inserted in to wall to give the bathroom a modern, minimal feel.
Bathroom went from old and dated to fresh and modern.
Total price for labour and materials: £5,000.
(This does not include bathroom suite or tiles)

22
case-studies/103.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: Modest Shower Room
location: Maldon, Essex
date: '2021-01-01'
features: [Accessible shower, False ceiling with spot lights]
price: £5,300
duration: 10 days
image: 0
images: 3
---
Medium sized bathroom for an elderly lady who had trouble getting in and out of the bath.
Complete refurbishment, bathroom was 20 years old and showed signs of damp.
Completely stripped out old bathroom, disposed of all rubbish and left to air
out with windows open for 3 days.
New bathroom installed with a fresh look and easy to maintain. 4 spot lights installed in false ceiling.
Total price for labour and materials: £5,300.
(This does not include bathroom suite or tiles)

19
case-studies/104.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: Period with a Modern Twist
location: Hampstead, London
date: '2020-10-01'
features: [Period features with a modern twist]
price: £8,800
duration: 3 weeks
image: 0
images: 3
---
A complete refurbishment in a three bedroom house in Hampstead, London.
Existing bathroom was more than 20 years old. We completely stripped out the old bathroom and disposed of all rubbish.
Client went for a Victorian style with a modern twist.
Total price for labour and materials: £8,800.
(This does not include bathroom suite or tiles)

18
case-studies/105.md Normal file
View File

@@ -0,0 +1,18 @@
---
title: En-suite
location: Chelmsford, Essex
date: '2020-12-01'
features: [Feature wall with an alcove shelf]
price: £7,400
duration: 11 days
image: 0
images: 2
---
2 bedroom flat Chelmsford. An en-suite bathroom. The existing bathroom was 10 years old and very dated.
Client went for fairly cheap products and tiles to keep within their budget.
Service included stripping out old bathroom and disposing of all rubbish, supplying and fitting everything.
Total price for labour and materials: £7,400.

16
case-studies/106.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: Metro style washroom
location: Ilford, London
date: '2021-02-01'
features: [Metro tiles]
price: £4,250
duration: 5 days
image: 0
images: 3
---
For this modest sized washroom, we stripped out the existing facilities and disposed of all rubbish, supplied and fitted a new toilet, sink and radiator.
We also tiled the room with metro style tiles for a modern look.
Total price for labour and materials: £4,250.

19
case-studies/107.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: Marble Effect Tiling
location: Harold Wood, Essex
date: '2021-02-01'
features: [Shower mixer, Spot lights, Tiled walls and bath panel]
price: £4,800
duration: 6 days
image: 0
images: 5
---
The old bathroom was stripped out and all rubbish removed.
The walls were tiled with marble effect tiles and new spot lights were installed in the ceiling.
The new bathroom was then installed, including a new shower mixer and towel radiator plumbed in to the wall.
Total price for labour and materials: £4,800.
(This does not include bathroom suite or tiles)

19
case-studies/108.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: Shower Room
location: Brentwood, Essex
date: '2021-02-01'
features: [Skylight feature]
price: £5,400
duration: 9 days
image: 9
images: 10
---
The old bathroom was stripped out and all rubbish removed.
Once empty, we treated the room for damp.
Room was tiled and painted before the new bathroom suite was installed.
Total price for labour and materials: £5,400.
(This does not include bathroom suite or tiles)

21
case-studies/109.md Normal file
View File

@@ -0,0 +1,21 @@
---
title: Mosaic Tiling
location: Brentwood, Essex
date: '2020-11-01'
features: [Electric alcove, Mosaic tiling, Under floor heating]
price: £6,500
duration: 12 days
image: 12
images: 13
---
Completely strip out old bathroom and dispose of all rubbish.
Relocate soil pipe to install toilet in new location.
Fitted electric under floor heating.
Installed mosaic wall tiles.
Total price for labour and materials: £6,500.
(This does not include bathroom suite or tiles)

19
case-studies/110.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: Victorian Look
location: Fulham, London
date: '2021-02-01'
features: [Walk in shower, Victorian style tiling]
price: £5,500
duration: 10 days
image: 3
images: 6
---
Stripped out old bathroom and disposed of all rubbish.
Tiled wall and floor with Victorian style tiling.
Recessed plumbing for wall mounted shower unit.
Total price for labour and materials: £5,500.
(This does not include bathroom suite or tiles)

19
case-studies/111.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: Two bathrooms
location: Ware, Hertfordshire
date: '2021-03-01'
features: [Free standing baths, Wall hung toilets, Spot lights]
price: £17,000
duration: 3 weeks
image: 11
images: 15
---
Stripped out old bathrooms and disposed of all rubbish.
Tiled both rooms and installed recessed spot lights.
Fitted free standing baths and wall mounted toilet basins.
Total price for labour and materials: £17,000.
(This does not include bathroom suites or tiles)

15
case-studies/112.md Normal file
View File

@@ -0,0 +1,15 @@
---
title: Modern Bathroom
location: Fulham, London
date: '2021-11-01'
features: [Shower, bath, vanity unit and toilet]
price: £8,650
duration: 3 weeks
image: 5
images: 9
---
Stripped out old bathrooms and disposed of all rubbish.
Total price for labour and materials: £8,650.
(This does not include bathroom suites or tiles)

19
case-studies/201.md Normal file
View File

@@ -0,0 +1,19 @@
---
title: New Extension
location: Eltham, London
date: '2020-11-01'
features: [Under floor heating]
price: £2,400
duration: 4 days
image: 3
images: 4
---
20 square meters of floor tiles laid in a new extension.
We fitted under floor heating before laying self levelling compound.
The tiles were also grouted and sealed.
The project took 4 days to complete.
Total price for labour and materials: £2,400.

20
case-studies/202.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: Ceramic Floor Tiles
location: Brentwood, Essex
date: '2020-12-01'
features: [Ceramic floor tiles]
price: £1,100
duration: 1 day
image: 3
images: 5
---
The client needed 19 square meters of ceramic floor tiles fitted at short notice.
We first applied self leveling compound as the concrete floor was quite uneven. After 2 days to allow for the compound to completely dry, we returned to install the tiles.
The installation was completed in one day and the client was able to continue with the kitchen installation the following day.
The client provided the ceramic floor tiles and we provided the levelling compound, adhesive and grout.
Total price for labour and materials: £1,100.

18
case-studies/203.md Normal file
View File

@@ -0,0 +1,18 @@
---
title: Period Floor Tiles
location: Barnes, London
date: '2020-10-01'
features: [Period style tiles]
price: £2,100
duration: 5 days
image: 0
images: 1
---
This room had old porcelain tiles, which we removed and disposed. We then applied self levelling compound to the floor and allowed to set for 24 hours.
All skirting and radiators were removed.
20 square meters of new tiles were laid and sealed for protection.
Total price for labour and materials: £2,100.

20
case-studies/204.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: Wood Effect Porcelain
location: Charlton, London
date: '2020-10-01'
features: [Wood effect porcelain tiles]
price: £3,100
duration: 7 days
image: 0
images: 1
---
Client needed old wooden floor removed. Once floor was removed 12mm concrete boards were laid to create a solid foundation.
50 square meters long porcelain tiles were laid sealed and grouted.
All rubbish was removed from site and included in the price.
We supplied all materials except tiles.
Total price for labour and materials: £3,100.

20
case-studies/205.md Normal file
View File

@@ -0,0 +1,20 @@
---
title: Period Floor Tiles
location: Chelsea, London
date: '2020-10-01'
features: [Under floor heating]
price: £5,600
duration: 2 weeks
image: 0
images: 1
---
Existing floor consisted of dated tiles, client wanted to replace them and install under floor heating at the same time.
Floor and adhesive was removed and disposed.
Electric under floor heating system was installed and levelling compound applied.
54 square meters new tiles were laid, grouted and sealed.
Total price for labour and materials: £5,600.

16
case-studies/206.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: Patterned Floor Tiles
location: Fulham, London
date: '2020-10-01'
features: [Under floor heating]
price: £4,500
duration: 1 week
image: 0
images: 1
---
Removed old flooring and adhesive and disposed of all rubbish.
Installed electric under floor heating and laid 35 square meters of new tiles.
Total price for labour and materials: £4,500.

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

@@ -1,10 +1,38 @@
import React from "react"; .block {
import styled from "@emotion/styled"; --grid-cols: 1;
import { css } from "@emotion/react";
const Container = styled.div`
display: grid; display: grid;
gap: var(--spacing-y-lg) var(--spacing-x-md); 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) =>
props.columns && props.columns &&
props.columns.map((column, i) => props.columns.map((column, i) =>
@@ -39,19 +67,4 @@ const Container = styled.div`
`} `}
`; `;
export default function Grid({ */
columns,
breakpoints = ["30em"],
verticalSeparator,
children,
}) {
return (
<Container
columns={columns}
breakpoints={breakpoints}
verticalSeparator={verticalSeparator}
>
{children}
</Container>
);
}

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

@@ -1,15 +1,38 @@
import React from "react"; .block {
import Img from "gatsby-image";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
const Container = styled.header`
position: relative; position: relative;
display: flex;
height: 50vh; height: 50vh;
min-height: 280px; min-height: 280px;
color: var(--color-white); color: var(--color-white);
${(props) => props.avoidNav && `margin-top: 40px;`} }
`;
.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` const Content = styled.div`
position: absolute; position: absolute;
${(props) => ${(props) =>
@@ -42,24 +65,4 @@ const Content = styled.div`
`; `;
const Map = styled.div``; const Map = styled.div``;
*/
export default function Header({
image,
map,
position = "middle middle",
avoidNav,
variant,
swap,
children,
}) {
const [posY, posX] = position.split(" ");
return (
<Container avoidNav={avoidNav}>
{image && <Img style={{ height: "100%" }} fluid={image} />}
{map && <Map>{map}</Map>}
<Content posX={posX} posY={posY}>
{children}
</Content>
</Container>
);
}

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

View File

@@ -1,109 +0,0 @@
const path = require("path");
const { GMAPS_APIKEY } = process.env;
const {
NODE_ENV,
URL: NETLIFY_SITE_URL = `https://abcprojectdesign.com`,
DEPLOY_PRIME_URL: NETLIFY_DEPLOY_URL = NETLIFY_SITE_URL,
CONTEXT: NETLIFY_ENV = NODE_ENV
} = process.env;
const isNetlifyProduction = NETLIFY_ENV === "production";
const siteUrl = isNetlifyProduction ? NETLIFY_SITE_URL : NETLIFY_DEPLOY_URL;
module.exports = {
siteMetadata: {
siteUrl,
title: `ABC Project Design`,
description: `Easy as ABC`,
keywords: `Bathroom Tiling Fitting Installation London Essex Suffolk Hertfordshire`,
secrets: { GMAPS_APIKEY },
navData: [
{
title: "Home",
to: "/"
},
{
title: "Services",
to: "/services",
subNav: [
{
title: "Bathrooms",
to: "/services/bathrooms"
},
{
title: "Tiling",
to: "/services/tiling"
}
]
},
{
title: "Area covered",
to: "/area-covered"
},
{
title: "About",
to: "/about"
},
{
title: "Contact",
to: "/contact"
}
]
},
plugins: [
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `ABC Project Design`,
short_name: `ABC`,
start_url: `/`,
icon: `src/images/icon.png`,
background_color: `#bbcbcb`,
theme_color: `#9c0905`,
display: `standalone`
}
},
{
resolve: `gatsby-plugin-robots-txt`,
options: {
resolveEnv: () => NETLIFY_ENV,
env: {
production: {
policy: [{ userAgent: `*` }]
},
"branch-deploy": {
policy: [{ userAgent: `*`, disallow: [`/`] }],
sitemap: null,
host: null
},
"deploy-preview": {
policy: [{ userAgent: `*`, disallow: [`/`] }],
sitemap: null,
host: null
}
}
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: path.join(__dirname, `src`, `images`)
}
},
{
resolve: "gatsby-plugin-react-svg",
options: {
rule: {
include: /icons/
}
}
},
`gatsby-plugin-sitemap`,
`gatsby-plugin-react-helmet`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
`gatsby-plugin-emotion`
]
};

View File

@@ -1,94 +0,0 @@
const { resolve } = require("path");
const reviews = require("./src/data/reviews.json");
const caseStudies = require("./src/data/case-studies.json");
const team = require("./src/data/team.json");
const padZero = n => (n < 10 ? `0${n}` : n);
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => {
team.team.forEach(person => {
const { name, email, phone, altPhone, title } = person;
const node = {
name,
email,
phone,
altPhone,
title,
id: createNodeId(`team-${name.replace(/ /g, "-")}`),
internal: {
type: "person",
contentDigest: createContentDigest(person)
}
};
actions.createNode(node);
});
reviews.reviews.forEach((review, i) => {
const { name, title, text, author, date } = review;
const node = {
name,
title,
text,
author,
date,
id: createNodeId(`review-${i}`),
internal: {
type: "review",
contentDigest: createContentDigest(review)
}
};
actions.createNode(node);
});
Object.keys(caseStudies).forEach(key => {
caseStudies[key].forEach((cs, i) => {
const {
title,
titleColor,
location,
image,
key_features,
price,
time_taken,
date,
content
} = cs;
const node = {
index: i,
path: `/services/${key}/case-study-${i + 1}`,
type: key,
title,
titleColor,
location,
image,
key_features,
price,
time_taken,
date,
content,
id: createNodeId(`${key}-${i + 1}`),
internal: {
type: "CaseStudy",
contentDigest: createContentDigest(cs)
}
};
actions.createNode(node);
});
});
};
exports.createPages = async ({ actions }) => {
const { createPage } = actions;
const template = resolve(__dirname, `src/case-study.js`);
Object.keys(caseStudies).forEach(key => {
caseStudies[key].forEach((cs, i) => {
const path = `/services/${key}/case-study-${i + 1}`;
createPage({
path,
component: template,
context: {
pagePath: path,
images: `/${key}-cs${padZero(i + 1)}(.*).jpg$/`
}
});
});
});
};

3
icons/arrow.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<path fill="currentColor" d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 515 B

3
icons/check.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7 425.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

3
icons/clock.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M256 512C114.6 512 0 397.4 0 256S114.6 0 256 0S512 114.6 512 256s-114.6 256-256 256zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"/>
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

Before

Width:  |  Height:  |  Size: 632 B

After

Width:  |  Height:  |  Size: 632 B

3
icons/facebook.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512">
<path fill="currentColor" d="M400 32H48A48 48 0 0 0 0 80v352a48 48 0 0 0 48 48h137.25V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.27c-30.81 0-40.42 19.12-40.42 38.73V256h68.78l-11 71.69h-57.78V480H400a48 48 0 0 0 48-48V80a48 48 0 0 0-48-48z"/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

25
icons/index.js Normal file
View File

@@ -0,0 +1,25 @@
export { default as ArrowSvg } from "./arrow.svg";
export { default as BarkSvg } from "./bark.svg";
export { default as CalendarSvg } from "./calendar.svg";
export { default as CheckCircleSvg } from "./check-circle.svg";
export { default as CheckSvg } from "./check.svg";
export { default as CheckatradeLogoSvg } from "./checkatrade-logo.svg";
export { default as CheckatradeSvg } from "./checkatrade.svg";
export { default as ClockSvg } from "./clock.svg";
export { default as EnvelopeSvg } from "./envelope.svg";
export { default as FacebookSvg } from "./facebook.svg";
export { default as HammerSvg } from "./hammer.svg";
export { default as LogoArrowsSvg } from "./logo-arrows.svg";
export { default as MobileSvg } from "./mobile.svg";
export { default as PenSvg } from "./pen.svg";
export { default as PhoneSvg } from "./phone.svg";
export { default as PoundSvg } from "./pound.svg";
export { default as QuoteLeftSvg } from "./quote-left.svg";
export { default as QuoteRightSvg } from "./quote-right.svg";
export { default as RatedPeopleLogoSvg } from "./rated-people-logo.svg";
export { default as RatedPeopleSvg } from "./rated-people.svg";
export { default as SmileSvg } from "./smile.svg";
export { default as SpySvg } from "./spy.svg";
export { default as TableListSvg } from "./table-list.svg";
export { default as TeamSvg } from "./team.svg";
export { default as XMarkSvg } from "./xmark.svg";

View File

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 328 B

4
icons/logo-arrows.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 64 32">
<path d="M 32.009279,0 A 2.3188402,2.3686159 0 0 0 30.376813,0.69637308 L 0,31.725241 H 6.539142 L 32,5.717838 57.460859,31.725241 H 64 L 33.623187,0.69637308 A 2.3188402,2.3686159 0 0 0 31.972192,0.004739 Z"/>
<path d="m 32.009279,12.790525 a 1.8318837,1.8617321 0 0 0 -1.261457,0.544781 L 12.382633,32 h 5.194175 L 32.046365,17.314584 46.515952,32 h 5.194205 L 33.344938,13.335306 a 1.8318837,1.8617321 0 0 0 -1.331005,-0.544781 z"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

3
icons/mobile.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<path fill="currentColor" d="M80 0C44.7 0 16 28.7 16 64V448c0 35.3 28.7 64 64 64H304c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H80zm80 432h64c8.8 0 16 7.2 16 16s-7.2 16-16 16H160c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 403 B

View File

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 423 B

View File

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

View File

Before

Width:  |  Height:  |  Size: 530 B

After

Width:  |  Height:  |  Size: 530 B

View File

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 526 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Some files were not shown because too many files have changed in this diff Show More