Migrate to Next
19
.eslintrc.js
@@ -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
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
4
.lintstagedrc.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"**/*.css": ["stylelint --fix", "prettier --write"],
|
||||||
|
"**/*.js": ["prettier --write"]
|
||||||
|
}
|
||||||
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.next
|
||||||
6
.stylelintrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["stylelint-prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": true
|
||||||
|
}
|
||||||
|
}
|
||||||
27
case-studies/101.md
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import styles from "./Button.module.css";
|
||||||
|
|
||||||
|
export default function Button({ children, type, href }) {
|
||||||
|
const isInternal = /^\/(?!\/)/.test(href);
|
||||||
|
if (isInternal) {
|
||||||
|
return (
|
||||||
|
<Link href={href} passHref>
|
||||||
|
<a className={styles.block}>{children}</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === "submit") {
|
||||||
|
return (
|
||||||
|
<button type="submit" className={styles.block}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a className={styles.block} href={href}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
components/Button.module.css
Normal file
@@ -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
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import styles from "./Card.module.css";
|
||||||
|
|
||||||
|
export default function Card({ image, heading, children, href }) {
|
||||||
|
return (
|
||||||
|
<Link href={href} passHref>
|
||||||
|
<a className={styles.block}>
|
||||||
|
{image && (
|
||||||
|
<span className={styles.media} aria-hidden="true">
|
||||||
|
<Image src={image} layout="fill" objectFit="fill" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={styles.content}>
|
||||||
|
{heading && <span className={styles.head}>{heading}</span>}
|
||||||
|
<span className={styles.body}>{children}</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
components/Card.module.css
Normal file
@@ -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
@@ -0,0 +1,89 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Icon, IconWithText, Text } from ".";
|
||||||
|
import {
|
||||||
|
CheckSvg,
|
||||||
|
ClockSvg,
|
||||||
|
CalendarSvg,
|
||||||
|
PoundSvg,
|
||||||
|
TableListSvg,
|
||||||
|
} from "../icons";
|
||||||
|
import styles from "./CaseStudy.module.css";
|
||||||
|
|
||||||
|
export default function CaseStudy({ data }) {
|
||||||
|
const { features, price, duration, date } = data;
|
||||||
|
const formattedDate = new Date(date).toLocaleString("en-GB", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.features}>
|
||||||
|
<IconWithText
|
||||||
|
icon={
|
||||||
|
<Icon>
|
||||||
|
<TableListSvg />
|
||||||
|
</Icon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text weight="bold" size="lg" noMargin>
|
||||||
|
Features:
|
||||||
|
</Text>
|
||||||
|
</IconWithText>
|
||||||
|
<ul className={styles.featurelist}>
|
||||||
|
{features.map((feature) => (
|
||||||
|
<li key={feature}>
|
||||||
|
<Icon size="lg" color="success">
|
||||||
|
<CheckSvg />
|
||||||
|
</Icon>
|
||||||
|
<span>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rest}>
|
||||||
|
<IconWithText
|
||||||
|
icon={
|
||||||
|
<Icon>
|
||||||
|
<PoundSvg />
|
||||||
|
</Icon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text weight="bold" size="lg" noMargin>
|
||||||
|
Total Cost:
|
||||||
|
</Text>
|
||||||
|
</IconWithText>
|
||||||
|
<Text noMargin>{price}</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rest}>
|
||||||
|
<IconWithText
|
||||||
|
icon={
|
||||||
|
<Icon>
|
||||||
|
<ClockSvg />
|
||||||
|
</Icon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text weight="bold" size="lg" noMargin>
|
||||||
|
Time taken:
|
||||||
|
</Text>
|
||||||
|
</IconWithText>
|
||||||
|
<Text noMargin>{duration}</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rest}>
|
||||||
|
<IconWithText
|
||||||
|
icon={
|
||||||
|
<Icon>
|
||||||
|
<CalendarSvg />
|
||||||
|
</Icon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text weight="bold" size="lg" noMargin>
|
||||||
|
Date:
|
||||||
|
</Text>
|
||||||
|
</IconWithText>
|
||||||
|
<Text noMargin>{formattedDate}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
components/CaseStudy.module.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.block {
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
background-color: var(--color-dark-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-sm) var(--spacing-lg);
|
||||||
|
max-width: var(--page-max-width);
|
||||||
|
margin-inline: auto;
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featurelist {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featurelist li {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rest {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40em) {
|
||||||
|
.features {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
components/ContactButton.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import styles from "./ContactButton.module.css";
|
||||||
|
|
||||||
|
export default function ContactButton({ children, href, size }) {
|
||||||
|
return (
|
||||||
|
<Link href={href} passHref>
|
||||||
|
<a
|
||||||
|
className={classnames(styles.block, {
|
||||||
|
[styles.sm]: size === "sm",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
components/ContactButton.module.css
Normal file
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Content.module.css";
|
||||||
|
|
||||||
|
export default function Content({ children }) {
|
||||||
|
return <div className={styles.block}>{children}</div>;
|
||||||
|
}
|
||||||
3
components/Content.module.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.block p {
|
||||||
|
margin: 0 0 var(--spacing-md);
|
||||||
|
}
|
||||||
54
components/Footer.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ContactButton, Grid, Icon, Text } from ".";
|
||||||
|
import { FacebookSvg, EnvelopeSvg, MobileSvg, PhoneSvg } from "../icons";
|
||||||
|
import styles from "./Footer.module.css";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
return (
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<Grid mdFillFit>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<ContactButton href="tel:+442071013958" size="sm">
|
||||||
|
<Icon size="lg">
|
||||||
|
<PhoneSvg />
|
||||||
|
</Icon>
|
||||||
|
02071013958
|
||||||
|
</ContactButton>
|
||||||
|
<ContactButton href="tel:+447378839358" size="sm">
|
||||||
|
<Icon size="lg">
|
||||||
|
<MobileSvg />
|
||||||
|
</Icon>
|
||||||
|
07378839358
|
||||||
|
</ContactButton>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<ContactButton
|
||||||
|
href="mailto:quotations@abcprojectdesign.com"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Icon size="lg">
|
||||||
|
<EnvelopeSvg />
|
||||||
|
</Icon>
|
||||||
|
quotations@abcprojectdesign.com
|
||||||
|
</ContactButton>
|
||||||
|
<ContactButton
|
||||||
|
href="https://www.facebook.com/people/ABC-Project-Design/100088268944294/"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Icon size="lg">
|
||||||
|
<FacebookSvg />
|
||||||
|
</Icon>
|
||||||
|
Facebook
|
||||||
|
</ContactButton>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
<Text size="xs">
|
||||||
|
©
|
||||||
|
{year} ABC Project Ltd
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
components/Footer.module.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.footer {
|
||||||
|
border-top: 1px solid #eaeaea;
|
||||||
|
padding: var(--spacing-lg) var(--spacing-sm) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
max-width: var(--page-max-width);
|
||||||
|
margin-inline: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
80
components/GalleryOverlay.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Icon } from ".";
|
||||||
|
import styles from "./GalleryOverlay.module.css";
|
||||||
|
import { ArrowSvg, XMarkSvg } from "../icons";
|
||||||
|
|
||||||
|
export default function GalleryOverlay({
|
||||||
|
onClose,
|
||||||
|
onPrev,
|
||||||
|
onNext,
|
||||||
|
image,
|
||||||
|
desc,
|
||||||
|
}) {
|
||||||
|
const headingRef = useRef();
|
||||||
|
const firstRef = useRef();
|
||||||
|
const lastRef = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
headingRef.current.focus();
|
||||||
|
}, []);
|
||||||
|
const handleKeyUp = (e) => {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 27:
|
||||||
|
onClose();
|
||||||
|
break;
|
||||||
|
case 37:
|
||||||
|
onPrev();
|
||||||
|
break;
|
||||||
|
case 39:
|
||||||
|
onNext();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={styles.block} onKeyUp={handleKeyUp}>
|
||||||
|
<button
|
||||||
|
className={styles.hidden}
|
||||||
|
onFocus={() => lastRef.current.focus()}
|
||||||
|
/>
|
||||||
|
<h1 className={styles.hidden} tabIndex="-1" ref={headingRef}>
|
||||||
|
{desc}
|
||||||
|
</h1>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<button
|
||||||
|
className={styles.prev}
|
||||||
|
aria-label="Previous image"
|
||||||
|
onClick={onPrev}
|
||||||
|
ref={firstRef}
|
||||||
|
>
|
||||||
|
<Icon size="lg" rotate="270">
|
||||||
|
<ArrowSvg />
|
||||||
|
</Icon>
|
||||||
|
</button>
|
||||||
|
<Image src={image} alt={desc} layout="fill" objectFit="contain" />
|
||||||
|
<button
|
||||||
|
className={styles.next}
|
||||||
|
aria-label="Next image"
|
||||||
|
onClick={onNext}
|
||||||
|
>
|
||||||
|
<Icon size="lg" rotate="90">
|
||||||
|
<ArrowSvg />
|
||||||
|
</Icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.close}
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={onClose}
|
||||||
|
ref={lastRef}
|
||||||
|
>
|
||||||
|
<Icon size="lg">
|
||||||
|
<XMarkSvg />
|
||||||
|
</Icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={styles.hidden}
|
||||||
|
onFocus={() => firstRef.current.focus()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
components/GalleryOverlay.module.css
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
.block {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
background-color: rgb(0, 0, 0, 0.75);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: var(--color-dark-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close,
|
||||||
|
.next,
|
||||||
|
.prev {
|
||||||
|
position: absolute;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:focus,
|
||||||
|
.next:focus,
|
||||||
|
.prev:focus {
|
||||||
|
outline: 2px solid var(--color-white);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next,
|
||||||
|
.prev {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
11
components/GalleryThumb.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import styles from "./GalleryThumb.module.css";
|
||||||
|
|
||||||
|
export default function GalleryThumb({ image, label, onClick }) {
|
||||||
|
return (
|
||||||
|
<button className={styles.block} onClick={onClick} aria-label={label}>
|
||||||
|
<Image src={image} layout="fill" objectFit="fill" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
components/GalleryThumb.module.css
Normal file
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -0,0 +1,26 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { Heading, Text } from ".";
|
||||||
|
import styles from "./HeaderLogo.module.css";
|
||||||
|
import { LogoArrowsSvg } from "../icons";
|
||||||
|
|
||||||
|
export default function HeaderLogo({ transition }) {
|
||||||
|
const [visible, setVisible] = useState(!transition);
|
||||||
|
useEffect(() => transition && setVisible(true), [transition]);
|
||||||
|
return (
|
||||||
|
<div className={classnames(styles.block, { [styles.isvisible]: visible })}>
|
||||||
|
<div className={styles.top}>
|
||||||
|
<LogoArrowsSvg />
|
||||||
|
</div>
|
||||||
|
<div className={styles.middle}>
|
||||||
|
<Heading>ABC Project Design Ltd</Heading>
|
||||||
|
<Text size="lg" noMargin>
|
||||||
|
Easy as ABC
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className={styles.bottom}>
|
||||||
|
<LogoArrowsSvg />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
components/HeaderLogo.module.css
Normal file
@@ -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
@@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import styles from "./Heading.module.css";
|
||||||
|
|
||||||
|
export default function Heading({ el: El = "h1", size, children }) {
|
||||||
|
return (
|
||||||
|
<El
|
||||||
|
className={classnames(styles.block, {
|
||||||
|
[styles.md]: size === "md",
|
||||||
|
[styles.lg]: size === "lg",
|
||||||
|
[styles.xl]: size === "xl",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</El>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
components/Heading.module.css
Normal file
@@ -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
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import styles from "./Icon.module.css";
|
||||||
|
|
||||||
|
export default function Icon({ children, size, rotate, color }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classnames(styles.block, {
|
||||||
|
[styles.sm]: size === "sm",
|
||||||
|
[styles.lg]: size === "lg",
|
||||||
|
[styles.xl]: size === "xl",
|
||||||
|
[styles.primary]: color === "primary",
|
||||||
|
[styles.dark]: color === "dark",
|
||||||
|
[styles.success]: color === "success",
|
||||||
|
[styles.rotate90]: rotate === "90",
|
||||||
|
[styles.rotate180]: rotate === "180",
|
||||||
|
[styles.rotate270]: rotate === "270",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
components/Icon.module.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.block {
|
||||||
|
display: inline-flex;
|
||||||
|
width: var(--icon-size-md);
|
||||||
|
height: var(--icon-side-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sm {
|
||||||
|
width: var(--icon-size-sm);
|
||||||
|
height: var(--icon-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg {
|
||||||
|
width: var(--icon-size-lg);
|
||||||
|
height: var(--icon-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.xl {
|
||||||
|
width: var(--icon-size-xl);
|
||||||
|
height: var(--icon-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: var(--color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate90 {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate180 {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate270 {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
11
components/IconWithText.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./IconWithText.module.css";
|
||||||
|
|
||||||
|
export default function IconWithText({ icon, children }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<div className={styles.icon}>{icon}</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
components/IconWithText.module.css
Normal file
@@ -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
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./InputField.module.css";
|
||||||
|
|
||||||
|
export default function InputField({ id, name, value, onChange, textArea }) {
|
||||||
|
const El = textArea ? "textarea" : "input";
|
||||||
|
return (
|
||||||
|
<El
|
||||||
|
className={styles.block}
|
||||||
|
type="text"
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
components/InputField.module.css
Normal file
@@ -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
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Interrupt.module.css";
|
||||||
|
|
||||||
|
export default function Interrupt() {
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<span className={styles.spinner}>
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
<span />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
components/Interrupt.module.css
Normal file
@@ -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
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Label.module.css";
|
||||||
|
|
||||||
|
export default function Label({ id, children }) {
|
||||||
|
return (
|
||||||
|
<label className={styles.block} htmlFor={id}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
components/Label.module.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
19
components/Layout.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import { Footer, Nav } from ".";
|
||||||
|
import styles from "./Layout.module.css";
|
||||||
|
|
||||||
|
export default function Layout({ title, children }) {
|
||||||
|
const pageTitle = `${title ? `${title} : ` : ""}ABC Project Design`;
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<Head>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<Nav />
|
||||||
|
<main className={styles.main}>{children}</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
components/Layout.module.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
20
components/Link.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
import NextLink from "next/link";
|
||||||
|
import styles from "./Link.module.css";
|
||||||
|
|
||||||
|
export default function Link({ href, children, ...rest }) {
|
||||||
|
if (/^\/(?!\/)/.test(href)) {
|
||||||
|
return (
|
||||||
|
<NextLink href={href} passHref>
|
||||||
|
<a className={styles.block} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</NextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<a className={styles.block} href={href} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
components/Link.module.css
Normal file
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Heading, Icon } from ".";
|
||||||
|
import { CheckatradeLogoSvg, QuoteLeftSvg, QuoteRightSvg } from "../icons";
|
||||||
|
import styles from "./Review.module.css";
|
||||||
|
|
||||||
|
export default function Review({ type, title, author, date, children }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.block}>
|
||||||
|
<div className={styles.head}>
|
||||||
|
<Icon size="xl">
|
||||||
|
<CheckatradeLogoSvg />
|
||||||
|
</Icon>
|
||||||
|
<Heading el="h3" size="lg">
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
|
<div className={styles.body}>
|
||||||
|
<div className={styles.prefix}>
|
||||||
|
<Icon size="lg">
|
||||||
|
<QuoteLeftSvg />
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
<div className={styles.suffix}>
|
||||||
|
<Icon size="lg">
|
||||||
|
<QuoteRightSvg />
|
||||||
|
</Icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
components/Review.module.css
Normal file
@@ -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
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import { Heading } from ".";
|
||||||
|
import styles from "./Section.module.css";
|
||||||
|
|
||||||
|
export default function Section({ variant, children, title }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classnames(styles.block, {
|
||||||
|
[styles.secondary]: variant === "secondary",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{title && (
|
||||||
|
<div className={styles.head}>
|
||||||
|
<Heading el="h2">{title}</Heading>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
components/Section.module.css
Normal file
@@ -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
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Select.module.css";
|
||||||
|
|
||||||
|
export default function Select({ id, name, value, options = [] }) {
|
||||||
|
return (
|
||||||
|
<select className={styles.block} id={id} name={name} value={value}>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<option value={opt.value} key={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
components/Select.module.css
Normal file
@@ -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
@@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import styles from "./Text.module.css";
|
||||||
|
|
||||||
|
export default function Text({
|
||||||
|
children,
|
||||||
|
alignment,
|
||||||
|
size,
|
||||||
|
weight,
|
||||||
|
color,
|
||||||
|
noMargin,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={classnames(styles.block, {
|
||||||
|
[styles.center]: alignment === "center",
|
||||||
|
[styles.right]: alignment === "right",
|
||||||
|
[styles.sm]: size === "sm",
|
||||||
|
[styles.md]: size === "md",
|
||||||
|
[styles.lg]: size === "lg",
|
||||||
|
[styles.xl]: size === "xl",
|
||||||
|
[styles.bold]: weight === "bold",
|
||||||
|
[styles.nomargin]: noMargin,
|
||||||
|
[styles.primary]: color === "primary",
|
||||||
|
[styles.dark]: color === "dark",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
components/Text.module.css
Normal file
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./Tile.module.css";
|
||||||
|
|
||||||
|
export default function Tile({ children }) {
|
||||||
|
return <div className={styles.block}>{children}</div>;
|
||||||
|
}
|
||||||
8
components/Tile.module.css
Normal file
@@ -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
@@ -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";
|
||||||
109
gatsby-config.js
@@ -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`
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -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
@@ -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 |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 471 B |
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 515 B |
3
icons/check.svg
Normal 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 |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
3
icons/clock.svg
Normal 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 |
|
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 632 B |
3
icons/facebook.svg
Normal 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 |
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
25
icons/index.js
Normal 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";
|
||||||
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
4
icons/logo-arrows.svg
Normal 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
@@ -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 |
|
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 423 B After Width: | Height: | Size: 423 B |
|
Before Width: | Height: | Size: 683 B After Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 530 B |
|
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 526 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |