folder structure
|
@ -0,0 +1,13 @@
|
||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
|
@ -0,0 +1,25 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/recommended'
|
||||||
|
],
|
||||||
|
// required to lint *.vue files
|
||||||
|
plugins: [
|
||||||
|
'vue'
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'vue/html-self-closing': 'off',
|
||||||
|
'vue/attributes-order': 'off',
|
||||||
|
'vue/max-attributes-per-line': 'off'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
# MarcLeopold
|
||||||
|
|
||||||
|
> Marc Leopold Website
|
||||||
|
|
||||||
|
## Build Setup
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# install dependencies
|
||||||
|
$ yarn install
|
||||||
|
|
||||||
|
# serve with hot reload at localhost:3000
|
||||||
|
$ yarn run dev
|
||||||
|
|
||||||
|
# build for production and launch server
|
||||||
|
$ yarn run build
|
||||||
|
$ yarn start
|
||||||
|
|
||||||
|
# generate static project
|
||||||
|
$ yarn run generate
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).
|
|
@ -0,0 +1,63 @@
|
||||||
|
import express from 'express'
|
||||||
|
const nodemailer = require('nodemailer')
|
||||||
|
const validator = require('validator')
|
||||||
|
const xssFilters = require('xss-filters')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
app.post('/', function (req, res) {
|
||||||
|
const attributes = ['name', 'email', 'msg']
|
||||||
|
const sanitizedAttributes = attributes.map(n => validateAndSanitize(n, req.body[n]))
|
||||||
|
const someInvalid = sanitizedAttributes.some(r => !r)
|
||||||
|
|
||||||
|
if (someInvalid) {
|
||||||
|
return res.status(400).json({ 'error': 'bad request'})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMail(...sanitizedAttributes)
|
||||||
|
return res.status(200).json({ 'message': 'success'})
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: '/api/contact',
|
||||||
|
handler: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAndSanitize (key, value) {
|
||||||
|
const rejectFunctions = {
|
||||||
|
name: v => v.length < 4,
|
||||||
|
email: v => !validator.isEmail(v),
|
||||||
|
msg: v => v.length < 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined || value.length < 1) { return false }
|
||||||
|
|
||||||
|
// if object has key and function returns false, return sanitised input.
|
||||||
|
// Else, return false
|
||||||
|
return rejectFunctions.hasOwnProperty(key) && !rejectFunctions[key](value) && xssFilters.inHTMLData(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMail (name, email, msg) {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
sendmail: true,
|
||||||
|
newline: 'unix',
|
||||||
|
path: '/usr/sbin/sendmail'
|
||||||
|
})
|
||||||
|
|
||||||
|
const text =
|
||||||
|
`Message from ${name}:
|
||||||
|
|
||||||
|
${msg}`
|
||||||
|
|
||||||
|
const mailJson = {
|
||||||
|
from: 'server@gabbaell.co.uk',
|
||||||
|
replyTo: email,
|
||||||
|
to: 'marcleopold.isnet@gabbaell.co.uk',
|
||||||
|
subject: 'Contact form message concerning Marc Leopold',
|
||||||
|
text: text,
|
||||||
|
}
|
||||||
|
|
||||||
|
transporter.sendMail(mailJson)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# ASSETS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your un-compiled assets such as LESS, SASS, or JavaScript.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked).
|
|
@ -0,0 +1,36 @@
|
||||||
|
// * {
|
||||||
|
// outline: 1px dotted red;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Raleway', sans-serif;
|
||||||
|
font-size: 100%;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
margin: 0 0 3rem;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, h3, h4, h5, h6 {
|
||||||
|
color: $color__neutral-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background-color: initial;
|
||||||
|
@include font-cursive();
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
$link: $color__primary-500;
|
||||||
|
$link-hover: $color__primary-700;
|
||||||
|
|
||||||
|
$primary: #f00;
|
||||||
|
$white: $color__neutral-900;
|
||||||
|
$danger: $color__accent-danger-300;
|
||||||
|
$success: $color__accent-success-300;
|
||||||
|
|
||||||
|
@import "~bulma/sass/utilities/_all.sass";
|
||||||
|
@import "~bulma/bulma.sass";
|
||||||
|
@import "~buefy/src/scss/buefy";
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: relative;
|
||||||
|
color: $color__neutral-700;
|
||||||
|
@include font-title(600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input, .textarea {
|
||||||
|
$color-bg: $color__neutral-900;
|
||||||
|
$gradient-from: .1;
|
||||||
|
$gradient-to: .3;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: rgba($color__neutral-900, .7);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
$color-bg: #fff;
|
||||||
|
$gradient-from: .1;
|
||||||
|
$gradient-to: .5;
|
||||||
|
|
||||||
|
background: $color-bg;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-to),
|
||||||
|
rgba($color-bg, $gradient-from),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
@include font-body(600);
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
@import 'palette';
|
||||||
|
@import 'mixins';
|
||||||
|
|
||||||
|
$bp__m: 40em;
|
||||||
|
|
||||||
|
// layout optomised for larger screens
|
||||||
|
$bp__layout: 40em;
|
||||||
|
|
||||||
|
// gallery ui becomes more compact
|
||||||
|
$bp__gallery-compact: 75em;
|
||||||
|
|
||||||
|
$z-index__page: 50;
|
||||||
|
$z-index__page-overlay: 75;
|
||||||
|
$z-index__menu: 100;
|
||||||
|
|
||||||
|
$site-menu__width: 20rem;
|
||||||
|
$site-menu__header-width: 3rem;
|
||||||
|
$site-menu__header-height: 3rem;
|
||||||
|
$site-menu__color-bg: $color__neutral-200;
|
||||||
|
|
||||||
|
$gallery-featured-width: 20rem;
|
||||||
|
$gallery-featured-width--compact: 12rem;
|
||||||
|
|
||||||
|
$gallery-thumbs-height: 8rem;
|
||||||
|
$gallery-thumbs-height--compact: 6rem;
|
|
@ -0,0 +1,14 @@
|
||||||
|
@mixin font-body($weight: 400) {
|
||||||
|
font-family: 'Raleway', sans-serif;
|
||||||
|
font-weight: $weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin font-title($weight: 400) {
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: $weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin font-cursive() {
|
||||||
|
font-family: 'Satisfy', cursive;
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
$color__primary-100: #090b10;
|
||||||
|
$color__primary-200: #242b3e;
|
||||||
|
$color__primary-300: #35405d;
|
||||||
|
$color__primary-400: #505f8c;
|
||||||
|
$color__primary-500: #6174aa;
|
||||||
|
$color__primary-600: #7d8db9;
|
||||||
|
$color__primary-700: #a8b3d0;
|
||||||
|
$color__primary-800: #c5cce0;
|
||||||
|
$color__primary-900: #e2e5ef;
|
||||||
|
|
||||||
|
$color__neutral-100: #060606;
|
||||||
|
$color__neutral-200: #121212;
|
||||||
|
$color__neutral-300: #212121;
|
||||||
|
$color__neutral-400: #353535;
|
||||||
|
$color__neutral-500: #494949;
|
||||||
|
$color__neutral-600: #717171;
|
||||||
|
$color__neutral-700: #9a9a9a;
|
||||||
|
$color__neutral-800: #c2c2c2;
|
||||||
|
$color__neutral-900: #eaeaea;
|
||||||
|
|
||||||
|
$color__accent-danger-100: #270202;
|
||||||
|
$color__accent-danger-300: #450403;
|
||||||
|
$color__accent-danger-500: #6b0504;
|
||||||
|
$color__accent-danger-700: #a05f5f;
|
||||||
|
$color__accent-danger-900: #d6baba;
|
||||||
|
|
||||||
|
$color__accent-success-100: #182816;
|
||||||
|
$color__accent-success-300: #264123;
|
||||||
|
$color__accent-success-500: #345830;
|
||||||
|
$color__accent-success-700: #7d947b;
|
||||||
|
$color__accent-success-900: #b5c2b3;
|
|
@ -0,0 +1,194 @@
|
||||||
|
@import 'buefy';
|
||||||
|
@import 'base';
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
@include font-title();
|
||||||
|
color: $color__neutral-900;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
transition: opacity 3s 2s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
@at-root .is-mounted & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
font-size: 3.7vw;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 90em) {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading {
|
||||||
|
margin: 1rem;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
width: 14em;
|
||||||
|
max-width: calc(50% - #{$site-menu__header-width} - 3rem);
|
||||||
|
margin: 2rem 1rem 1rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-overlay {
|
||||||
|
$color: $color__neutral-100; // color of overlay
|
||||||
|
|
||||||
|
box-shadow: 0 0 12px 0 $color__neutral-100 inset,
|
||||||
|
0 2px 12px 3px rgba($color__neutral-100, .5);
|
||||||
|
|
||||||
|
transition: filter 1s;
|
||||||
|
filter: grayscale(.5);
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
box-shadow: 0 0 12px 0 $color__neutral-400 inset,
|
||||||
|
0 2px 12px 3px rgba($color__neutral-300, .5);
|
||||||
|
|
||||||
|
filter: grayscale(.95);
|
||||||
|
&:hover {
|
||||||
|
filter: grayscale(.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color, .5),
|
||||||
|
rgba($color, 0),
|
||||||
|
rgba($color, .7)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
to left,
|
||||||
|
rgba($color, .2),
|
||||||
|
rgba($color, 0),
|
||||||
|
rgba($color, .2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
padding: .4em .8em;
|
||||||
|
color: $color__neutral-900;
|
||||||
|
border: 2px solid $color__neutral-900;
|
||||||
|
@include font-title();
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background-color: $color__neutral-200;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom left,
|
||||||
|
$color__neutral-400,
|
||||||
|
$color__neutral-200
|
||||||
|
);
|
||||||
|
|
||||||
|
transition: opacity .3s;
|
||||||
|
opacity: .4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:link, &:visited {
|
||||||
|
color: $color__neutral-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-tint {
|
||||||
|
background-color: rgba($color__primary-100, .7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-deco {
|
||||||
|
$color: rgba($color__neutral-100, .3);
|
||||||
|
|
||||||
|
background-color: $color;
|
||||||
|
box-shadow: 0 0 64px 64px $color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content-text {
|
||||||
|
color: $color__neutral-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-indicator {
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
transition: opacity 1s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
border: 1px solid $color__neutral-700;;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active::before {
|
||||||
|
transition: opacity 2s .2s;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-active, .page-leave-active {
|
||||||
|
transition: opacity .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter, .page-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nuxt-progress {
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0px" y="0px"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
xml:space="preserve"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon points="207.093,30.187 176.907,0 48.907,128 176.907,256 207.093,225.813 109.28,128"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
fill="#000000"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="6px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 522 B |
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 350.39741 351.03559"
|
||||||
|
y="0px"
|
||||||
|
x="0px"
|
||||||
|
>
|
||||||
|
<path d="m 91.878859,32.707107 83.892861,83.892873 82.61784,-82.617833 58.66388,58.663883 -82.61783,82.61783 83.25469,83.2547 -58.98035,58.98035 -83.2547,-83.25469 L 91.37099,318.32848 32.707107,259.6646 116.79137,175.58033 32.898502,91.687463 Z"
|
||||||
|
fill-opacity="0.5"
|
||||||
|
fill="#000000"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="15px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 609 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-rolling" style="background: none;"><circle cx="50" cy="50" fill="none" ng-attr-stroke="{{config.color}}" ng-attr-stroke-width="{{config.width}}" ng-attr-r="{{config.radius}}" ng-attr-stroke-dasharray="{{config.dasharray}}" stroke="#ff7c00" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138" transform="rotate(173.999 50 50)"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animateTransform></circle></svg>
|
After Width: | Height: | Size: 687 B |
|
@ -0,0 +1,201 @@
|
||||||
|
<template>
|
||||||
|
<div class="background-image-loader">
|
||||||
|
<div class="background-container">
|
||||||
|
<div class="background background-default"></div>
|
||||||
|
<transition name="fade-fast"
|
||||||
|
>
|
||||||
|
<div v-if="loadedImageUrl !== null"
|
||||||
|
class="background background-img background-blur"
|
||||||
|
:style="backgroundBlurStyle"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<transition name="fade"
|
||||||
|
@beforeEnter="handleBeforeEnter"
|
||||||
|
@afterLeave="handleAfterLeave"
|
||||||
|
>
|
||||||
|
<div class="background background-img"
|
||||||
|
:key="loadedImageUrl"
|
||||||
|
:style="backgroundStyle"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
<slot name="overlay">
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import imageLoader from '~/mixins/imageLoader.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [ imageLoader ],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
imageUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: function () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadedImageUrl: null,
|
||||||
|
isInTransition: false,
|
||||||
|
transitionTimeout: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
backgroundStyle () {
|
||||||
|
let style = {}
|
||||||
|
if (this.loadedImageUrl) {
|
||||||
|
style.backgroundImage = `url(${this.loadedImageUrl})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundBlurStyle () {
|
||||||
|
let style = {}
|
||||||
|
if (this.loadedImageUrl) {
|
||||||
|
style.backgroundImage = `url(${this.loadedImageUrl})`
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
imageUrl () {
|
||||||
|
if (!this.isInTransition && this.imageUrl) {
|
||||||
|
this.setImage(this.imageUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.setImage(this.imageUrl)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setImage(url) {
|
||||||
|
if (url === null) { return }
|
||||||
|
this.loadImage(url)
|
||||||
|
.then(img => {
|
||||||
|
this.loadedImageUrl = img.src
|
||||||
|
this.$emit('imageLoaded', img)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.$emit('imageLoadError', err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleBeforeEnter() {
|
||||||
|
// TOOD get the actual value programatically
|
||||||
|
// use ref and ref.style.transitionDuration or whatever
|
||||||
|
let transitionDuration = 4000
|
||||||
|
|
||||||
|
this.isInTransition = true
|
||||||
|
|
||||||
|
clearTimeout(this.transitionTimeout)
|
||||||
|
this.transitionTimeout = setTimeout(() => {
|
||||||
|
this.isInTransition = false
|
||||||
|
if (this.imageUrl !== this.loadedImageUrl) {
|
||||||
|
this.setImage(this.imageUrl)
|
||||||
|
}
|
||||||
|
}, transitionDuration)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAfterLeave() {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.background-image-loader {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-img {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-default {
|
||||||
|
$shadow-opacity: .6;
|
||||||
|
$shadow-color: $color__neutral-100;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($shadow-color, $shadow-opacity),
|
||||||
|
rgba($shadow-color, 0) 20%,
|
||||||
|
rgba($shadow-color, 0) 80%,
|
||||||
|
rgba($shadow-color, $shadow-opacity)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($shadow-color, $shadow-opacity),
|
||||||
|
rgba($shadow-color, 0) 20%,
|
||||||
|
rgba($shadow-color, 0) 80%,
|
||||||
|
rgba($shadow-color, $shadow-opacity)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to top,
|
||||||
|
$color__neutral-100,
|
||||||
|
$color__neutral-300
|
||||||
|
)
|
||||||
|
;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-blur {
|
||||||
|
filter: blur(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
$timing: .7;
|
||||||
|
&-enter-active {
|
||||||
|
transition: opacity 4s * $timing 1s * $timing ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
transition: opacity 2s * $timing ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter, &-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-fast {
|
||||||
|
$timing: 1;
|
||||||
|
&-enter-active {
|
||||||
|
transition: opacity 3s * $timing ease-in;
|
||||||
|
transition-delay: .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<BackgroundImageLoader :image-url="imageUrls[activeIndex]"
|
||||||
|
@imageLoaded="handleImageLoaded"
|
||||||
|
>
|
||||||
|
<template slot="overlay">
|
||||||
|
<slot name="overlay"></slot>
|
||||||
|
</template>
|
||||||
|
</BackgroundImageLoader>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import imageLoader from '~/mixins/imageLoader.js'
|
||||||
|
import BackgroundImageLoader from '@/components/BackgroundImageLoader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BackgroundImageLoader,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [ imageLoader ],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
imageUrls: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
activeIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
currentlyLoadingIndex: 1,
|
||||||
|
errorUrls: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
if (this.imageUrls.length > 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.preloadImage()
|
||||||
|
}, 100) // timeout so events get fired - don't know why that is yet TODO - investigate properly
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
preloadImage () {
|
||||||
|
this.loadImage(this.imageUrls[this.currentlyLoadingIndex])
|
||||||
|
.then(img => {
|
||||||
|
this.$emit('imageLoaded', img)
|
||||||
|
this.currentlyLoadingIndex++
|
||||||
|
if (this.currentlyLoadingIndex < this.imageUrls.length) {
|
||||||
|
this.preloadImage()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// TODO handle error cases
|
||||||
|
console.log('imageloaded ERROR', err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageLoaded () {
|
||||||
|
this.$emit('imageLoaded', this.imageUrls[this.activeIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<article class="content-page"
|
||||||
|
:class="{ 'is-mounted': isMounted }"
|
||||||
|
>
|
||||||
|
<h1 v-if="heading !== ''" class="page-heading page-title">{{ heading }}</h1>
|
||||||
|
<section class="content-container load-transition">
|
||||||
|
<div v-if="$slots.default" class="content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div v-if="$slots.background" class="background">
|
||||||
|
<slot name="background"></slot>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMounted: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isMounted = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$z-index-bottom: 5;
|
||||||
|
$z-index-top: 10;
|
||||||
|
|
||||||
|
.content-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: $site-menu__header-width 0 0 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: 0 0 0 $site-menu__header-width;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: $color__neutral-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
z-index: $z-index-top;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
padding: .5rem 1rem 2rem;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
width: 50%;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
color: #fff; // TEMP
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-heading {
|
||||||
|
z-index: $z-index-top + 5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
z-index: $z-index-bottom;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
opacity: .5;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-transition {
|
||||||
|
transition: 2s 2s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
@at-root .is-mounted & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<ul class="gallery-featured" ref="galleryList">
|
||||||
|
<li v-for="(gallery, index) in galleries"
|
||||||
|
ref="galleries"
|
||||||
|
class="featured-image thumb-overlay selected-indicator"
|
||||||
|
:class="{ 'is-active': index === galleryActive }"
|
||||||
|
:style="{ 'background-image': 'url(' + gallery.featuredImage + ')' }"
|
||||||
|
:key="index"
|
||||||
|
@click="$emit('clicked', index)">
|
||||||
|
<span class="gallery-title">{{ gallery.title }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
galleries: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
galleryActive: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
if (this.galleryActive > 0) {
|
||||||
|
const el = this.$refs.galleries[this.galleryActive]
|
||||||
|
el.parentNode.scrollTop = el.offsetTop
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.featured-image {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $bp__layout) {
|
||||||
|
.featured-image {
|
||||||
|
height: calc(50vh - #{$site-menu__header-height / 2});
|
||||||
|
width: 100vw;
|
||||||
|
flex: 0 0 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-title {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
max-width: 66%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
font-size: 1.5em;
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-overlay {
|
||||||
|
$color: $color__neutral-100; // color of overlay
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color, .9),
|
||||||
|
rgba($color, .5),
|
||||||
|
rgba($color, .5)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: linear-gradient(
|
||||||
|
to left,
|
||||||
|
rgba($color, .4),
|
||||||
|
rgba($color, 0),
|
||||||
|
rgba($color, .4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.gallery-featured {
|
||||||
|
transition: opacity .3s; // TEMP
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-image {
|
||||||
|
z-index: 10;
|
||||||
|
position: relative;
|
||||||
|
height: 0;
|
||||||
|
padding-top: 100%;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-title {
|
||||||
|
z-index: 15;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 1em;
|
||||||
|
@include font-title();
|
||||||
|
color: $color__neutral-900;
|
||||||
|
|
||||||
|
@media (min-width: $bp__gallery-compact) {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,328 @@
|
||||||
|
<template>
|
||||||
|
<div class="image-viewer"
|
||||||
|
:class="{ 'is-visible': isVisible }">
|
||||||
|
<transition name="trans-bg-image">
|
||||||
|
<div v-if="backgroundImageUrl !== null"
|
||||||
|
:key="backgroundImageUrl"
|
||||||
|
class="viewer-background"
|
||||||
|
:style="backgroundStyle"
|
||||||
|
></div>
|
||||||
|
</transition>
|
||||||
|
<div class="image-container loading-container">
|
||||||
|
<transition name="trans-bg-image">
|
||||||
|
<span v-if="showLoading"
|
||||||
|
class="text-loading animation-pulse"
|
||||||
|
>
|
||||||
|
Loading
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<div class="image-container">
|
||||||
|
<transition name="trans-image" mode="out-in">
|
||||||
|
<img v-if="displayImageUrl !== null"
|
||||||
|
:key="displayImageUrl"
|
||||||
|
class="image image-shadow"
|
||||||
|
:src="displayImageUrl"
|
||||||
|
>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<div class="close-viewer mobile-only"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<SVGIcon />
|
||||||
|
</div>
|
||||||
|
<ThumbNav v-if="hasPrev"
|
||||||
|
class="thumb-nav thumb-nav--left mobile-only"
|
||||||
|
direction="left"
|
||||||
|
@navClick="$emit('clickPrev')"/>
|
||||||
|
<ThumbNav v-if="hasNext"
|
||||||
|
class="thumb-nav thumb-nav--right mobile-only"
|
||||||
|
direction="right"
|
||||||
|
@navClick="$emit('clickNext')"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ThumbNav from '@/components/ThumbNav'
|
||||||
|
import imageLoader from '~/mixins/imageLoader.js'
|
||||||
|
import SVGIcon from '@/assets/svg/close-wide.svg'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ThumbNav,
|
||||||
|
SVGIcon,
|
||||||
|
},
|
||||||
|
mixins: [ imageLoader ],
|
||||||
|
props: {
|
||||||
|
isVisible: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
imageUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasNext: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
hasPrev: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loadingImageUrl: 'https://via.placeholder.com/120x120',
|
||||||
|
backgroundImageUrl: null, // blurred vwersion of image that makes up the background
|
||||||
|
displayImageUrl: null, // image being viewed
|
||||||
|
showLoading: true,
|
||||||
|
loadingTimeout: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
backgroundStyle () {
|
||||||
|
return {
|
||||||
|
backgroundImage: 'url(' + this.backgroundImageUrl + ')'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
imageUrl () {
|
||||||
|
this.setImages(this.imageUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.setImages(this.imageUrl)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setImages(url) {
|
||||||
|
this.displayImageUrl = null
|
||||||
|
this.showLoading = false
|
||||||
|
this.loadingTimeout = setTimeout(() => {
|
||||||
|
this.showLoading = true
|
||||||
|
}, 1000)
|
||||||
|
this.loadImage(this.imageUrl)
|
||||||
|
.then(img => {
|
||||||
|
this.displayImageUrl = img.src
|
||||||
|
clearTimeout(this.loadingTimeout)
|
||||||
|
this.showLoading = false
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.backgroundImageUrl = img.src
|
||||||
|
})
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
// TODO catch errors
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.image-viewer {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
position: absolute;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-shadow {
|
||||||
|
box-shadow: 2px 4px 12px -2px rgba($color__neutral-200, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-loading {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: $color__neutral-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-pulse {
|
||||||
|
animation: pulse 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $bp__layout) {
|
||||||
|
.close-viewer {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
top: calc(#{$site-menu__header-height} + 2px);
|
||||||
|
right: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: opacity .3s .2s;
|
||||||
|
opacity: .2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .4;
|
||||||
|
animation: rotate .3s ease-in-out 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-viewer {
|
||||||
|
z-index: 50;
|
||||||
|
|
||||||
|
transition: opacity 1s; //TEMP
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-nav {
|
||||||
|
position: absolute;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
opacity: .2;
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
left: 0;
|
||||||
|
padding-right: 30%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--right {
|
||||||
|
right: 0;
|
||||||
|
padding-left: 30%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-viewer {
|
||||||
|
padding: 1rem 1rem 1rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: calc(3rem + 8px);
|
||||||
|
right: $gallery-featured-width--compact;
|
||||||
|
bottom: 8px;
|
||||||
|
background-position: top center;
|
||||||
|
|
||||||
|
@media (min-width: $bp__gallery-compact) {
|
||||||
|
right: $gallery-featured-width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-position: center center;
|
||||||
|
filter: blur(100px);
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
padding-bottom: $gallery-thumbs-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trans-image {
|
||||||
|
&-enter-active {
|
||||||
|
transition: opacity 1s .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
transition: opacity .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter, &-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trans-bg-image {
|
||||||
|
&-enter-active, &-leave-active {
|
||||||
|
transition: opacity .8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter, &-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 50%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0%, 100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate(3deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate(-1deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: rotate(1deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,285 @@
|
||||||
|
<template>
|
||||||
|
<div class="gallery-page"
|
||||||
|
:class="{'is-mounted': isMounted }"
|
||||||
|
>
|
||||||
|
<GalleryImageViewer class="image-viewer"
|
||||||
|
ref="imageViewer"
|
||||||
|
:is-visible="imageViewerIsVisible"
|
||||||
|
:image-url="viewingImageUrl"
|
||||||
|
:has-next="activeImageIndex < galleries[activeGalleryIndex].images.length - 1"
|
||||||
|
:has-prev="activeImageIndex > 0"
|
||||||
|
@clickPrev="handleClickPrev"
|
||||||
|
@clickNext="handleClickNext"
|
||||||
|
@close="imageViewerIsVisible = false" />
|
||||||
|
<article class="gallery">
|
||||||
|
<h1 class="page-heading page-title"><span class="shadow-deco">{{ title }}</span></h1>
|
||||||
|
<GalleryFeatured class="gallery__featured load-transition load-transition--1"
|
||||||
|
:galleries="galleries"
|
||||||
|
:gallery-active="activeGalleryIndex"
|
||||||
|
@clicked="handleFeaturedClick" />
|
||||||
|
<GalleryThumbs class="gallery__thumbs load-transition load-transition--2"
|
||||||
|
:featured-height="featuredImageHeight"
|
||||||
|
:galleries="galleries"
|
||||||
|
:active-row="activeRow"
|
||||||
|
:active-index="activeImageIndex"
|
||||||
|
:show-image="showImage"
|
||||||
|
@thumbClick="handleThumbClick"/>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GalleryFeatured from '@/components/GalleryFeatured'
|
||||||
|
import GalleryThumbs from '@/components/GalleryThumbs'
|
||||||
|
import GalleryImageViewer from '@/components/GalleryImageViewer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GalleryFeatured,
|
||||||
|
GalleryThumbs,
|
||||||
|
GalleryImageViewer,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
galleries: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 'My Galleries'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMounted: false,
|
||||||
|
featuredImageHeight: '16rem',
|
||||||
|
imageViewerIsVisible: false,
|
||||||
|
activeRow: 0,
|
||||||
|
activeGalleryIndex: 0,
|
||||||
|
activeImageIndex: 0,
|
||||||
|
showImage: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
viewingImageUrl () {
|
||||||
|
if (
|
||||||
|
this.galleries[this.activeGalleryIndex]
|
||||||
|
&& this.galleries[this.activeGalleryIndex].images
|
||||||
|
&& this.galleries[this.activeGalleryIndex].images[this.activeImageIndex]
|
||||||
|
) {
|
||||||
|
return this.galleries[this.activeGalleryIndex].images[this.activeImageIndex].url
|
||||||
|
} else {
|
||||||
|
// TOOD return 404 page - does this do it?
|
||||||
|
throw({ statusCode: 404, message: 'Image Not Found' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$route () {
|
||||||
|
const query = this.$route.query
|
||||||
|
if (!query || !query.gallery) { return }
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const gallery = parseInt(query.gallery)
|
||||||
|
const image = parseInt(query.image) || 0
|
||||||
|
|
||||||
|
if (gallery !== this.activeGalleryIndex) {
|
||||||
|
this.activeRow = gallery
|
||||||
|
this.activeGalleryIndex = gallery
|
||||||
|
}
|
||||||
|
if (image !== this.activeImageIndex) {
|
||||||
|
this.activeImageIndex = image
|
||||||
|
this.showImage = image
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created () {
|
||||||
|
let query = this.$route.query
|
||||||
|
this.activeGalleryIndex = query.gallery ? parseInt(query.gallery) : 0
|
||||||
|
this.activeRow = this.activeGalleryIndex
|
||||||
|
this.activeImageIndex = query.image ? parseInt(query.image) : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('resize', () => { this.imageViewerIsVisible = false })
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isMounted = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleFeaturedClick (index) {
|
||||||
|
this.activeRow = index
|
||||||
|
this.activeImageIndex = 0
|
||||||
|
this.activeGalleryIndex = index
|
||||||
|
this.setQueryString()
|
||||||
|
},
|
||||||
|
handleThumbClick(gallery, image) {
|
||||||
|
this.activeGalleryIndex = gallery
|
||||||
|
this.activeImageIndex = image
|
||||||
|
this.imageViewerIsVisible = true
|
||||||
|
this.setQueryString()
|
||||||
|
},
|
||||||
|
handleClickNext () {
|
||||||
|
this.activeImageIndex++;
|
||||||
|
},
|
||||||
|
handleClickPrev () {
|
||||||
|
this.activeImageIndex--;
|
||||||
|
},
|
||||||
|
setQueryString () {
|
||||||
|
this.$router.push({
|
||||||
|
path: this.$route.path,
|
||||||
|
query: {
|
||||||
|
gallery: this.activeGalleryIndex,
|
||||||
|
image: this.activeImageIndex,
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.gallery-page {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-viewer {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $bp__layout) {
|
||||||
|
.page-heading {
|
||||||
|
z-index: 5;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 2rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-page {
|
||||||
|
padding-top: $site-menu__header-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__thumbs {
|
||||||
|
z-index: 10;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__featured {
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-viewer {
|
||||||
|
font-size: 10em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.page-heading {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
max-width: calc(100% - 3rem - #{$gallery-featured-width--compact});
|
||||||
|
|
||||||
|
@media (min-width: $bp__gallery-compact) {
|
||||||
|
max-width: calc(100% - 2rem - #{$gallery-featured-width});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-page {
|
||||||
|
padding-left: $site-menu__header-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__featured {
|
||||||
|
order: 2;
|
||||||
|
flex: 0 0 $gallery-featured-width--compact;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (min-width: $bp__gallery-compact) {
|
||||||
|
flex-basis: $gallery-featured-width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__thumbs {
|
||||||
|
order: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-left: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-viewer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-transition {
|
||||||
|
transition: opacity 2s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&--1 {
|
||||||
|
transition-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--2 {
|
||||||
|
transition-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .is-mounted & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,464 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="gallery-thumbs__spacer"></div>
|
||||||
|
<ul ref="galleryThumbsList"
|
||||||
|
class="gallery-thumbs__list list-shadow">
|
||||||
|
<li v-for="(gallery, galleryIndex) in galleries"
|
||||||
|
:key="galleryIndex"
|
||||||
|
:class="{ 'is-active': activeRow === galleryIndex }"
|
||||||
|
class="gallery-thumbs__row-container">
|
||||||
|
<ul ref="galleryThumbsRows"
|
||||||
|
class="gallery-thumbs__row"
|
||||||
|
:style="{ 'transform': 'translate3d(' + thumbRowOffsets[galleryIndex] + 'px, 0, 0)' }">
|
||||||
|
<li class="featured-image-spacer mobile-only"
|
||||||
|
:style="{ 'height': featuredHeight }">
|
||||||
|
<ThumbNav class="featured-nav mobile-only"
|
||||||
|
direction="right"
|
||||||
|
@navClick="handleNavClick(galleryIndex, 'right')" />
|
||||||
|
</li>
|
||||||
|
<li v-for="(image, imageIndex) in gallery.images"
|
||||||
|
:ref="galleryIndex == 0 && imageIndex == 0 ? 'thumbContainer' : null"
|
||||||
|
class="thumb-container thumb-overlay selected-indicator"
|
||||||
|
:class="{ 'is-active': imageIndex === activeIndex }"
|
||||||
|
:style="{ 'background-image': 'url(' + image.thumbUrl + ')' }"
|
||||||
|
:key="galleryIndex + '.' + imageIndex + '.' + image.url"
|
||||||
|
@click="$emit('thumbClick', galleryIndex, imageIndex)">
|
||||||
|
<ThumbNav class="thumb-nav thumb-nav--left mobile-only"
|
||||||
|
direction="left"
|
||||||
|
@navClick="handleNavClick(galleryIndex, 'left')" />
|
||||||
|
<ThumbNav v-if="imageIndex < gallery.images.length - 1"
|
||||||
|
class="thumb-nav mobile-only"
|
||||||
|
direction="right"
|
||||||
|
@navClick="handleNavClick(galleryIndex, 'right')" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<ThumbNav class="thumb-nav thumb-nav--left mobile-hide shadow-deco"
|
||||||
|
:class="{ 'is-active': isLeftDesktopNavActive }"
|
||||||
|
direction="left"
|
||||||
|
@navClick="handleNavClickDesktop(activeRow, 'left')"/>
|
||||||
|
<ThumbNav class="thumb-nav thumb-nav--right mobile-hide shadow-deco"
|
||||||
|
:class="{ 'is-active': isRightDesktopNavActive }"
|
||||||
|
direction="right"
|
||||||
|
@navClick="handleNavClickDesktop(activeRow, 'right')"/>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ThumbNav from '@/components/ThumbNav'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ThumbNav
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
galleries: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
featuredHeight: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
activeRow: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// index of image to be highlighted
|
||||||
|
activeIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// scroll this image into view
|
||||||
|
showImage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
thumbRowOffsets: new Array(this.galleries.length),
|
||||||
|
isLeftDesktopNavActive: false,
|
||||||
|
resizeIsThrottling: false,
|
||||||
|
isThumbsScrolling: false,
|
||||||
|
touchDownPosition: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
thumbsVerticalOffset () {
|
||||||
|
let rowHeight = this.$refs.thumbContainer ? this.$refs.thumbContainer[0].clientHeight : 0;
|
||||||
|
return rowHeight * this.activeRow
|
||||||
|
},
|
||||||
|
isRightDesktopNavActive () {
|
||||||
|
return this.thumbRowOffsets[this.activeRow] > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
activeRow: function(val) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateElements(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
showImage () {
|
||||||
|
this.scrollToThumb(this.showImage)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
this.$refs.galleryThumbsList.addEventListener('touchstart', this.handleTouch, { passive: true })
|
||||||
|
this.$refs.galleryThumbsList.addEventListener('touchend', this.handleTouch, { passive: true })
|
||||||
|
this.$refs.galleryThumbsList.addEventListener('wheel', this.handleThumbsScroll, { passive: true })
|
||||||
|
// firefox
|
||||||
|
this.$refs.galleryThumbsList.addEventListener('mousewheel', this.handleThumbsScroll, { passive: true })
|
||||||
|
this.$nextTick(function () {
|
||||||
|
this.updateElements()
|
||||||
|
this.scrollToThumb(this.activeIndex)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// TOOD move logic of getting sizes of elements into getter methods
|
||||||
|
scrollToThumb (thumbIndex) {
|
||||||
|
if (thumbIndex >= this.galleries[this.activeRow].images.length || thumbIndex < 0) { return }
|
||||||
|
const containerWidth = this.$refs.galleryThumbsList.clientWidth
|
||||||
|
const thumbWidth = this.$refs.thumbContainer[0].clientWidth
|
||||||
|
let offset = this.thumbRowOffsets[this.activeRow]
|
||||||
|
const thumbPosition = thumbWidth * (this.galleries[this.activeRow].images.length - thumbIndex)
|
||||||
|
const rowWidth = this.$refs.galleryThumbsRows[this.activeRow].clientWidth
|
||||||
|
|
||||||
|
if (containerWidth < thumbPosition - offset) {
|
||||||
|
this.$set(this.thumbRowOffsets, this.activeRow, thumbPosition - containerWidth + 1)
|
||||||
|
this.isLeftDesktopNavActive = containerWidth + offset > rowWidth
|
||||||
|
} else if (thumbPosition <= offset) {
|
||||||
|
this.$set(this.thumbRowOffsets, this.activeRow, thumbPosition - thumbWidth)
|
||||||
|
this.isLeftDesktopNavActive = containerWidth + offset > rowWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
updateElements (resetOffsets=true) {
|
||||||
|
if (resetOffsets) {this.resetOffsets()}
|
||||||
|
if (this.$refs.galleryThumbsRows[this.activeRow] && this.$refs.galleryThumbsList) {
|
||||||
|
let totalRowWidth = this.$refs.galleryThumbsRows[this.activeRow].clientWidth
|
||||||
|
let visibleRowWidth = this.$refs.galleryThumbsList.clientWidth
|
||||||
|
this.initLeftNav(totalRowWidth, visibleRowWidth)
|
||||||
|
if (visibleRowWidth > totalRowWidth) {
|
||||||
|
this.centerThumbs(totalRowWidth, visibleRowWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initLeftNav (totalRowWidth, visibleRowWidth) {
|
||||||
|
let offset = this.thumbRowOffsets[this.activeRow]
|
||||||
|
let maxOffset = totalRowWidth - visibleRowWidth
|
||||||
|
this.isLeftDesktopNavActive = (totalRowWidth > visibleRowWidth)
|
||||||
|
&& (offset < maxOffset)
|
||||||
|
},
|
||||||
|
resetOffsets() {
|
||||||
|
for (let index = 0; index < this.thumbRowOffsets.length; index++) {
|
||||||
|
this.$set(this.thumbRowOffsets, index, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
centerThumbs(rowWidth, visibleWidth) {
|
||||||
|
let offset = (visibleWidth - rowWidth) / -2
|
||||||
|
this.$set(this.thumbRowOffsets, [this.activeRow], offset)
|
||||||
|
},
|
||||||
|
handleNavClick(source, direction) {
|
||||||
|
let offset = this.thumbRowOffsets[source]
|
||||||
|
const thumbContainerWidth = this.$refs.thumbContainer[0].clientWidth
|
||||||
|
if (direction === 'left') {
|
||||||
|
offset += thumbContainerWidth
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
offset -= thumbContainerWidth
|
||||||
|
}
|
||||||
|
offset = offset > 0 ? 0 : offset
|
||||||
|
this.$set(this.thumbRowOffsets, source, offset)
|
||||||
|
},
|
||||||
|
handleNavClickDesktop(source, direction) {
|
||||||
|
let offset = this.thumbRowOffsets[source]
|
||||||
|
let thumbContainerWidth = this.$refs.thumbContainer[0].clientWidth
|
||||||
|
let visibleRowWidth = this.$refs.galleryThumbsList.clientWidth
|
||||||
|
let totalRowWidth = this.$refs.galleryThumbsRows[source].clientWidth
|
||||||
|
let maxOffset = totalRowWidth - visibleRowWidth
|
||||||
|
if (direction === 'left') {
|
||||||
|
offset += thumbContainerWidth
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
offset -= thumbContainerWidth
|
||||||
|
if (offset != 0) {
|
||||||
|
this.isLeftDesktopNavActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (offset < 0) {
|
||||||
|
offset = 0
|
||||||
|
} else if (offset >= maxOffset) {
|
||||||
|
offset = maxOffset
|
||||||
|
this.isLeftDesktopNavActive = false
|
||||||
|
}
|
||||||
|
this.$set(this.thumbRowOffsets, source, offset)
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
if (!this.resizeIsThrottling) {
|
||||||
|
this.resizeIsThrottling = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.updateElements()
|
||||||
|
this.resizeIsThrottling = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleThumbsScroll(event) {
|
||||||
|
if (event.wheelDeltaX) {
|
||||||
|
if (event.wheelDeltaX > 0) {
|
||||||
|
this.doThumbScroll('right')
|
||||||
|
} else if (event.wheelDeltaX < 0) {
|
||||||
|
this.doThumbScroll('left')
|
||||||
|
}
|
||||||
|
} else if (event.deltaY) {
|
||||||
|
if (event.deltaY > 0) {
|
||||||
|
this.doThumbScroll('left')
|
||||||
|
} else if (event.deltaY < 0) {
|
||||||
|
this.doThumbScroll('right')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleTouch(event) {
|
||||||
|
if (event.type === 'touchstart') {
|
||||||
|
this.touchDownPosition = event.changedTouches[0].clientX
|
||||||
|
} else if (event.type === 'touchmove') {
|
||||||
|
event.preventDefault()
|
||||||
|
} else if (event.type === 'touchend') {
|
||||||
|
let xPos = event.changedTouches[0].clientX
|
||||||
|
let direction = this.touchDownPosition - xPos < 0 ? 'left' : 'right'
|
||||||
|
this.doThumbScroll(direction)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doThumbScroll(direction) {
|
||||||
|
if (this.isThumbsScrolling) { return }
|
||||||
|
|
||||||
|
this.isThumbsScrolling = true
|
||||||
|
if ((direction === 'left' && this.isLeftDesktopNavActive) || (direction === 'right' && this.isRightDesktopNavActive)) {
|
||||||
|
this.handleNavClickDesktop(this.activeRow, direction)
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isThumbsScrolling = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.gallery-thumbs__row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-container {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
padding: 0 2px;
|
||||||
|
background-clip: content-box;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $bp__layout) {
|
||||||
|
.gallery-thumbs__spacer {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbs__row {
|
||||||
|
position: relative;
|
||||||
|
transition : transform .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-image-spacer {
|
||||||
|
position: relative;
|
||||||
|
height: calc(50vh - #{$site-menu__header-height / 2}) !important; // must override inline style set with prop
|
||||||
|
width: 100vw;
|
||||||
|
flex: 0 0 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-container {
|
||||||
|
position: relative;
|
||||||
|
height: calc(50vh - #{$site-menu__header-height / 2});
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-nav {
|
||||||
|
z-index: 20;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-left: 80%;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-nav {
|
||||||
|
z-index: 20;
|
||||||
|
position: absolute;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 8px;
|
||||||
|
padding: 0 1rem 0 3rem;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
opacity: .4;
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
left: 8px;
|
||||||
|
right: auto;
|
||||||
|
padding: 0 3rem 0 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.mobile-only {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hide {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-image-spacer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: $gallery-thumbs-height--compact;
|
||||||
|
width: $gallery-thumbs-height--compact * 1.6;
|
||||||
|
flex: 0 0 $gallery-thumbs-height--compact * 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__thumbs {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbs__list {
|
||||||
|
position: relative;
|
||||||
|
height: $gallery-thumbs-height--compact;
|
||||||
|
flex: 0 0 $gallery-thumbs-height--compact;
|
||||||
|
bottom: 8px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: opacity .3s; // TEMP
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-shadow {
|
||||||
|
$color: $color__neutral-100;
|
||||||
|
box-shadow: 0 -3px 33px -9px $color, 0 0 8px -3px $color;
|
||||||
|
background-color: rgba($color, .7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbs__row-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
transition: opacity .5s .3s;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbs__row {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
transition: transform .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
width: $gallery-thumbs-height--compact - 2rem;
|
||||||
|
height: $gallery-thumbs-height--compact - 2rem;
|
||||||
|
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--left {
|
||||||
|
left: -.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--right {
|
||||||
|
right: -.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__gallery-compact) {
|
||||||
|
.gallery-thumbs__list {
|
||||||
|
height: $gallery-thumbs-height;
|
||||||
|
flex: 0 0 $gallery-thumbs-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-container {
|
||||||
|
height: $gallery-thumbs-height;
|
||||||
|
width: $gallery-thumbs-height * 1.6;
|
||||||
|
flex: 0 0 $gallery-thumbs-height * 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-thumbs__list {
|
||||||
|
height: $gallery-thumbs-height;
|
||||||
|
flex: 0 0 $gallery-thumbs-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-nav {
|
||||||
|
width: $gallery-thumbs-height - 2rem;
|
||||||
|
height: $gallery-thumbs-height - 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="VueToNuxtLogo">
|
||||||
|
<div class="Triangle Triangle--two"/>
|
||||||
|
<div class="Triangle Triangle--one"/>
|
||||||
|
<div class="Triangle Triangle--three"/>
|
||||||
|
<div class="Triangle Triangle--four"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.VueToNuxtLogo {
|
||||||
|
display: inline-block;
|
||||||
|
animation: turn 2s linear forwards 1s;
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 180px;
|
||||||
|
width: 245px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Triangle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Triangle--one {
|
||||||
|
border-left: 105px solid transparent;
|
||||||
|
border-right: 105px solid transparent;
|
||||||
|
border-bottom: 180px solid #41b883;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Triangle--two {
|
||||||
|
top: 30px;
|
||||||
|
left: 35px;
|
||||||
|
animation: goright 0.5s linear forwards 3.5s;
|
||||||
|
border-left: 87.5px solid transparent;
|
||||||
|
border-right: 87.5px solid transparent;
|
||||||
|
border-bottom: 150px solid #3b8070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Triangle--three {
|
||||||
|
top: 60px;
|
||||||
|
left: 35px;
|
||||||
|
animation: goright 0.5s linear forwards 3.5s;
|
||||||
|
border-left: 70px solid transparent;
|
||||||
|
border-right: 70px solid transparent;
|
||||||
|
border-bottom: 120px solid #35495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Triangle--four {
|
||||||
|
top: 120px;
|
||||||
|
left: 70px;
|
||||||
|
animation: godown 0.5s linear forwards 3s;
|
||||||
|
border-left: 35px solid transparent;
|
||||||
|
border-right: 35px solid transparent;
|
||||||
|
border-bottom: 60px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes turn {
|
||||||
|
100% {
|
||||||
|
transform: rotateX(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes godown {
|
||||||
|
100% {
|
||||||
|
top: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes goright {
|
||||||
|
100% {
|
||||||
|
left: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<article class="is-mounted no-content-page no-content-bg">
|
||||||
|
<h1 class="no-content-heading page-title">{{ heading }}</h1>
|
||||||
|
<p class="no-content-text">{{ message }}</p>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NoContent',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
heading: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default () {
|
||||||
|
return 'This page blah blah ...'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.no-content-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: $site-menu__header-height * 2 1rem 0;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: 0 0 0 $site-menu__header-width;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content-bg {
|
||||||
|
$color-inner: $color__primary-500;
|
||||||
|
$color-outer: $color__primary-100;
|
||||||
|
$transparency-outer: .1;
|
||||||
|
$transparency-inner: .05;
|
||||||
|
|
||||||
|
background-color: rgba($color-inner, .1);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color-inner, $transparency-inner),
|
||||||
|
rgba($color-outer, $transparency-outer)
|
||||||
|
);
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color-outer, $transparency-outer),
|
||||||
|
rgba($color-inner, $transparency-inner),
|
||||||
|
rgba($color-outer, $transparency-outer)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color-outer, $transparency-outer),
|
||||||
|
rgba($color-inner, $transparency-inner),
|
||||||
|
rgba($color-outer, $transparency-outer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content-heading {
|
||||||
|
margin-bottom: .25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# COMPONENTS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
The components directory contains your Vue.js Components.
|
||||||
|
|
||||||
|
_Nuxt.js doesn't supercharge these components._
|
|
@ -0,0 +1,629 @@
|
||||||
|
<template>
|
||||||
|
<section :class="{ 'is-open': isOpen,
|
||||||
|
'is-mounted': isMounted
|
||||||
|
}"
|
||||||
|
class="menu-drawer menu-layout"
|
||||||
|
>
|
||||||
|
<div class="menu-header__item menu-toggle"
|
||||||
|
@click="$emit('toggleMenu')"
|
||||||
|
>
|
||||||
|
<div class="menu-bars"></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-content">
|
||||||
|
<div class="menu-close"
|
||||||
|
@click="$emit('closeMenu')"
|
||||||
|
>
|
||||||
|
<span class="menu-close__content">close</span>
|
||||||
|
</div>
|
||||||
|
<nav class="menu-content__body">
|
||||||
|
<ul class="site-nav">
|
||||||
|
<li class="site-nav__item"
|
||||||
|
v-for="item in siteNav"
|
||||||
|
:key="item.to"
|
||||||
|
@click="$emit('closeMenu')"
|
||||||
|
>
|
||||||
|
<div v-if="item.bgImgUrl"
|
||||||
|
class="menu-background menu-link-background"
|
||||||
|
:style="{ 'background-image': loadMenuImages ? `url(${item.bgImgUrl})` : 'none' }"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<nuxt-link class="site-nav__link"
|
||||||
|
:to="item.to"
|
||||||
|
:exact="item.to === '/'"
|
||||||
|
>
|
||||||
|
{{ item.text }}
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="site-nav__footer social-nav">
|
||||||
|
<li v-for="item in socialNav"
|
||||||
|
class="social-nav-item"
|
||||||
|
:key="item.to"
|
||||||
|
>
|
||||||
|
<div class="menu-background social-background">
|
||||||
|
<b-icon class="social-background__icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<a class="social-nav__link"
|
||||||
|
:href="item.to"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<b-icon class="social-nav__icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<footer class="menu-content__footer">
|
||||||
|
<p class="footer-attr">Copyright © {{ new Date().getFullYear() }} Marc Leopold Photography</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="menu-header"
|
||||||
|
@click="$emit('toggleMenu')"
|
||||||
|
>
|
||||||
|
<div class="menu-header__inner">
|
||||||
|
<span class="menu-header__item site-title">Marc Leopold Photography</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMounted: false, //component has loaded
|
||||||
|
loadMenuImages: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
siteNav () {
|
||||||
|
return this.$store.getters['navigation/siteNav']
|
||||||
|
},
|
||||||
|
|
||||||
|
socialNav () {
|
||||||
|
return this.$store.getters['navigation/socialNav']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
const mq = window.matchMedia("(min-width: 40em)")
|
||||||
|
this.loadMenuImages = mq.matches
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isMounted = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$transition-timing: .5s;
|
||||||
|
|
||||||
|
.menu-drawer {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
right: 0;
|
||||||
|
top: -100%;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
transition: transform $transition-timing;
|
||||||
|
transform: translate3d(0, $site-menu__header-width, 0);
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
transform: translate3d(0, 100%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
width: $site-menu__width;
|
||||||
|
top: 0;
|
||||||
|
right: 100%;
|
||||||
|
transform: translate3d($site-menu__header-width, 0, 0);
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
$color-bg: $site-menu__color-bg;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
background-color: $site-menu__color-bg;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
darken($color-bg, 2%),
|
||||||
|
$color-bg,
|
||||||
|
darken($color-bg, 2%)
|
||||||
|
);
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
darken($color-bg, 2%),
|
||||||
|
$color-bg,
|
||||||
|
darken($color-bg, 2%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
align-items: normal;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content__footer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em 0 0;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header {
|
||||||
|
$color-bg: $color__neutral-300;
|
||||||
|
$color-shade: darken($color__neutral-300, 5);
|
||||||
|
// $color-shade: $color__neutral-200;
|
||||||
|
|
||||||
|
z-index: 20;
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 $site-menu__header-height;
|
||||||
|
|
||||||
|
color: $color__neutral-900;
|
||||||
|
background-color: $color-bg;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
$color-bg,
|
||||||
|
$color-shade
|
||||||
|
);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
max-width: $site-menu__header-width;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
$color-shade,
|
||||||
|
$color-bg,
|
||||||
|
$color-bg,
|
||||||
|
$color-shade
|
||||||
|
);
|
||||||
|
opacity: .97;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header__inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: 0 $site-menu__header-width 0 1rem;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 100%;
|
||||||
|
width: 100vh;
|
||||||
|
height: $site-menu__header-width;
|
||||||
|
|
||||||
|
transform-origin: 100% 0;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.1;
|
||||||
|
|
||||||
|
transition: opacity 2s 1s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
@at-root .is-mounted & {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include font-title($weight: 400);
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
letter-spacing: 1.3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-close {
|
||||||
|
z-index: 30;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
@include font-title();
|
||||||
|
text-transform: lowercase;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $color__neutral-800;
|
||||||
|
|
||||||
|
transition: opacity 0 $transition-timing;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@at-root .menu.is-open & {
|
||||||
|
transition: opacity 1s $transition-timing + .2s;
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $bp__layout - .01em) {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-close__content {
|
||||||
|
display: block;
|
||||||
|
padding: .5rem .75rem 1rem 1rem;
|
||||||
|
|
||||||
|
transition: opacity .2s;
|
||||||
|
opacity: .6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav {
|
||||||
|
font-size: 1.4em;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
font-size: .8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav__item {
|
||||||
|
z-index: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav__link {
|
||||||
|
z-index: 5;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding: .2em 0;
|
||||||
|
|
||||||
|
color: $color__neutral-800;
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: .9;
|
||||||
|
|
||||||
|
&:link, &:visited {
|
||||||
|
opacity: .9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nuxt-link-active {
|
||||||
|
pointer-events: none;
|
||||||
|
color: $color__neutral-900;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: .5em 0;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
$size: .2rem;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: $size;
|
||||||
|
width: $size;
|
||||||
|
right: 100%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin-top: .02rem;
|
||||||
|
margin-right: .3rem;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background-color: $color__neutral-600;
|
||||||
|
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nuxt-link-active::before {
|
||||||
|
transition: opacity 1s .5s;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav__footer {
|
||||||
|
margin-top: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav {
|
||||||
|
z-index: 10;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav__link {
|
||||||
|
display: block;
|
||||||
|
height: 2em;
|
||||||
|
width: 5ch;
|
||||||
|
|
||||||
|
transition: opacity .5s;
|
||||||
|
color: $color__neutral-900;
|
||||||
|
|
||||||
|
&:link, &:visited {
|
||||||
|
opacity: .8;
|
||||||
|
color: $color__neutral-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
width: 3ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav__icon.icon {
|
||||||
|
justify-content: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-attr {
|
||||||
|
color: $color__neutral-600;
|
||||||
|
font-size: .7em;
|
||||||
|
@include font-body(600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-toggle {
|
||||||
|
$width: 2rem;
|
||||||
|
$margin: ($site-menu__header-width - $width) / 2;
|
||||||
|
|
||||||
|
z-index: 30;
|
||||||
|
position: absolute;
|
||||||
|
width: $width;
|
||||||
|
height: $width;
|
||||||
|
right: $margin;
|
||||||
|
bottom: $margin;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: opacity .2s;
|
||||||
|
opacity: .6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
bottom: auto;
|
||||||
|
top: $margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bars {
|
||||||
|
$color: $color__neutral-900;
|
||||||
|
$width: 4px;
|
||||||
|
$padding: 4px; // padding on top/bottom
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: $padding;
|
||||||
|
bottom: $padding;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
border-top: $width solid $color;
|
||||||
|
border-bottom: $width solid $color;
|
||||||
|
|
||||||
|
transition: transform .3s .2s ease-out;
|
||||||
|
transform: scale(1, .7);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: $width;
|
||||||
|
left: 0;
|
||||||
|
top: calc(50% - #{$width / 2});
|
||||||
|
|
||||||
|
background-color: $color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .menu.is-open & {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-background {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.menu-background {
|
||||||
|
z-index: 1;
|
||||||
|
display: initial;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
right: $site-menu__header-width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link-background {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
$color: $site-menu__color-bg; //color of gradient overlay
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color, 0),
|
||||||
|
rgba($color, .7),
|
||||||
|
rgba($color, .7),
|
||||||
|
rgba($color, 0)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color, 1),
|
||||||
|
rgba($color, .2) 50%,
|
||||||
|
rgba($color, 0) 90%,
|
||||||
|
rgba($color, 1)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color, 1),
|
||||||
|
rgba($color, 0) 20%,
|
||||||
|
rgba($color, 0) 80%,
|
||||||
|
rgba($color, 1) 90%,
|
||||||
|
rgba($color, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav__item:hover .menu-link-background {
|
||||||
|
transition: opacity 1s;
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-background {
|
||||||
|
filter: blur(3px);
|
||||||
|
transition: opacity .5s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-background__icon {
|
||||||
|
position: absolute;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav-item:hover .social-background {
|
||||||
|
transition: opacity 1s;
|
||||||
|
opacity: .15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
.menu-content .social-nav__icon i::before {
|
||||||
|
font-size: 1.1em;
|
||||||
|
filter: blur(20xp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-background__icon i.mdi {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
font-size: 19em;
|
||||||
|
margin-bottom: -.05em;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
margin-bottom: -.19em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($site-menu__color-bg, .8),
|
||||||
|
rgba($site-menu__color-bg, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="wrapper"
|
||||||
|
@click.stop="$emit('navClick')"
|
||||||
|
>
|
||||||
|
<div class="svg-container"
|
||||||
|
:class="{ 'is-reversed': direction === 'right' }"
|
||||||
|
>
|
||||||
|
<SVGIcon class="svg-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SVGIcon from '@/assets/svg/chevron-left.svg'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SVGIcon,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
direction: {
|
||||||
|
type: String,
|
||||||
|
default: function () {
|
||||||
|
return 'right'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-container {
|
||||||
|
transition: opacity .3s, transform .5s;
|
||||||
|
opacity: .6;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
@at-root .wrapper:hover & {
|
||||||
|
opacity: .6;
|
||||||
|
transform: translate3d(-3px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .wrapper:hover &.is-reversed {
|
||||||
|
transform: translate3d(3px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 4rem;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 4rem;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-reversed .svg-icon {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,7 @@
|
||||||
|
# LAYOUTS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Application Layouts.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/views#layouts).
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page">
|
||||||
|
<div class="page-overlay"
|
||||||
|
:class="{ 'is-active': isMenuOpen }"
|
||||||
|
@click="closeMenu"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<nuxt />
|
||||||
|
</div>
|
||||||
|
<SiteMenu class="menu"
|
||||||
|
:is-open="isMenuOpen"
|
||||||
|
@toggleMenu="toggleMenu"
|
||||||
|
@closeMenu="closeMenu"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SiteMenu from '~/components/SiteMenu'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SiteMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isMenuOpen: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleMenu () {
|
||||||
|
this.isMenuOpen = !this.isMenuOpen
|
||||||
|
},
|
||||||
|
closeMenu () {
|
||||||
|
this.isMenuOpen = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.page-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
z-index: $z-index__page;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-overlay {
|
||||||
|
$color: $color__primary-100;
|
||||||
|
z-index: $z-index__page-overlay;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
background-color: rgba($color, .6);
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color, 1),
|
||||||
|
rgba($color, 1) $site-menu__width + 3rem,
|
||||||
|
rgba($color, .8),
|
||||||
|
rgba($color, 1)
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 70px 40px $color inset;
|
||||||
|
|
||||||
|
transition: opacity .5s .2s;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
&.is-active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
z-index: $z-index__menu;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="error-page">
|
||||||
|
<h1 class="heading" v-if="error.statusCode === 404">Page not found!</h1>
|
||||||
|
<h1 class="heading" v-else>An error occurred!</h1>
|
||||||
|
<nav>
|
||||||
|
<ul class="site-nav">
|
||||||
|
<li class="site-nav__item"
|
||||||
|
v-for="item in siteNav"
|
||||||
|
:key="item.to"
|
||||||
|
@click="$emit('closeMenu')"
|
||||||
|
>
|
||||||
|
<nuxt-link :to="item.to" >
|
||||||
|
{{ item.text }}
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
error: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
siteNav () {
|
||||||
|
return this.$store.getters['navigation/siteNav']
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.error-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding: calc(1rem + #{$site-menu__header-height}) 1rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: 6rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: $color__accent-danger-700;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav__item {
|
||||||
|
margin-bottom: .2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# MIDDLEWARE
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your application middleware.
|
||||||
|
The middleware lets you define custom function to be ran before rendering a page or a group of pages (layouts).
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
|
|
@ -0,0 +1,16 @@
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
loadImage(url) {
|
||||||
|
return new Promise( (resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
img.addEventListener('load', e => resolve(img));
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
reject(new Error(`Failed to load image URL: ${url}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
img.src = url;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
const pkg = require('./package')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'universal',
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 3003,
|
||||||
|
host: '0.0.0.0'
|
||||||
|
},
|
||||||
|
serverMiddleware: [
|
||||||
|
'~/api/contact',
|
||||||
|
],
|
||||||
|
/*
|
||||||
|
** Headers of the page
|
||||||
|
*/
|
||||||
|
head: {
|
||||||
|
title: pkg.name,
|
||||||
|
titleTemplate: 'Marc Leopold | %s',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
|
||||||
|
{ hid: 'description', name: 'description', content: 'Photographer, Marc Leopold has images in numerous collections and publications. Here is a glimpse of his work, an insight into his philosophy and motivations.' },
|
||||||
|
|
||||||
|
{ property: 'og:image', content: 'https://marcleopold.isnet.uk/img/open-graph/marc-leopold-ss.png'},
|
||||||
|
{ property: 'og:image:width', content: '1200'},
|
||||||
|
{ property: 'og:image:height', content: '600'},
|
||||||
|
{ property: 'og:image:type', content: 'image/png' },
|
||||||
|
{ property: 'og:title', content: 'Marc Leopold Photography' },
|
||||||
|
{ property: 'og:url', content: 'https://marcleopold.isnet.uk' },
|
||||||
|
{ property: 'og:site_name', content: 'Marc Leopold Photography' },
|
||||||
|
{ property: 'og:type', content: 'website'},
|
||||||
|
{ property: 'og:description', content: 'Marc Leopold is a Chicago born photographer whose images have appeared in many collections and publications. His website is a showcase of his outstanding work and an insight into his philosophy and motivations.' },
|
||||||
|
{ name: 'twitter:card', content: 'summary' },
|
||||||
|
{ name: 'twitter:title', content: 'Marc Leopold Photography' },
|
||||||
|
{ name: 'twitter:creator', content: '@studiovxweb' },
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },
|
||||||
|
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
|
||||||
|
{ rel: 'icon', type: 'image/png', sizes: '96x96', href: '/favicon-96x96.png' },
|
||||||
|
{ rel: 'stylesheet',
|
||||||
|
href: 'https://fonts.googleapis.com/css?family=' +
|
||||||
|
'Montserrat:400,600|' +
|
||||||
|
'Raleway:400,600|' +
|
||||||
|
'Satisfy'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Customize the progress-bar color
|
||||||
|
*/
|
||||||
|
loading: {
|
||||||
|
color: '#fff',
|
||||||
|
height: '1px',
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Global CSS
|
||||||
|
*/
|
||||||
|
css: [
|
||||||
|
'@/assets/scss/style.scss'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Plugins to load before mounting the App
|
||||||
|
*/
|
||||||
|
plugins: [
|
||||||
|
{ src: '~/plugins/Vuelidate' }
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Nuxt.js modules
|
||||||
|
*/
|
||||||
|
modules: [
|
||||||
|
// Doc: https://github.com/nuxt-community/axios-module#usage
|
||||||
|
'@nuxtjs/axios',
|
||||||
|
// Doc: https://buefy.github.io/#/documentation
|
||||||
|
'nuxt-buefy',
|
||||||
|
[
|
||||||
|
'nuxt-sass-resources-loader',
|
||||||
|
[
|
||||||
|
'@/assets/scss/_globals.scss'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'@nuxtjs/proxy',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Axios module configuration
|
||||||
|
*/
|
||||||
|
axios: {
|
||||||
|
// See https://github.com/nuxt-community/axios-module#options
|
||||||
|
proxy: true,
|
||||||
|
baseURL: 'http://192.168.0.5:3003',
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
proxy: {
|
||||||
|
'/api/v1/': 'http://192.168.0.5:8101'
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Build configuration
|
||||||
|
*/
|
||||||
|
build: {
|
||||||
|
/*
|
||||||
|
** You can extend webpack config here
|
||||||
|
*/
|
||||||
|
extend(config, ctx) {
|
||||||
|
// vue-svg-loader
|
||||||
|
const svgRule = config.module.rules.find(rule => rule.test.test('.svg'))
|
||||||
|
svgRule.test = /\.(png|jpe?g|gif|webp)$/
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.svg$/,
|
||||||
|
loader: 'vue-svg-loader',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Run ESLint on save
|
||||||
|
if (ctx.isDev && ctx.isClient) {
|
||||||
|
config.module.rules.push({
|
||||||
|
enforce: 'pre',
|
||||||
|
test: /\.(js|vue)$/,
|
||||||
|
loader: 'eslint-loader',
|
||||||
|
exclude: /(node_modules)/
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "MarcLeopold",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Marc Leopold Website",
|
||||||
|
"author": "studio v/x",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt start",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||||
|
"precommit": "npm run lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxtjs/axios": "^5.3.6",
|
||||||
|
"@nuxtjs/proxy": "^1.3.1",
|
||||||
|
"cross-env": "^5.2.0",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"nodemailer": "^5.1.1",
|
||||||
|
"nuxt": "^2.0.0",
|
||||||
|
"nuxt-buefy": "^0.2.1",
|
||||||
|
"nuxt-sass-resources-loader": "^2.0.5",
|
||||||
|
"validator": "^10.11.0",
|
||||||
|
"vuelidate": "^0.7.4",
|
||||||
|
"xss-filters": "^1.2.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-eslint": "^8.2.1",
|
||||||
|
"eslint": "^5.0.1",
|
||||||
|
"eslint-loader": "^2.0.0",
|
||||||
|
"eslint-plugin-vue": "^4.0.0",
|
||||||
|
"node-sass": "^4.11.0",
|
||||||
|
"nodemon": "^1.11.0",
|
||||||
|
"sass-loader": "^7.1.0",
|
||||||
|
"vue-svg-loader": "^0.11.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
# PAGES
|
||||||
|
|
||||||
|
This directory contains your Application Views and Routes.
|
||||||
|
The framework reads all the `*.vue` files inside this directory and create the router of your application.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing).
|
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<ContentPage :heading="title" class="about">
|
||||||
|
<div class="about-content" :class="{ 'no-content-container': isNoContent }">
|
||||||
|
<h2 class="heading heading-top">{{ title }}</h2>
|
||||||
|
<div v-html="body"></div>
|
||||||
|
</div>
|
||||||
|
<BackgroundImageLoader slot="background"
|
||||||
|
class="background"
|
||||||
|
:image-url="imageUrl"
|
||||||
|
>
|
||||||
|
<div slot="overlay" class="background-tint background-overlay"></div>
|
||||||
|
</BackgroundImageLoader>
|
||||||
|
</ContentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContentPage from '@/components/ContentPage'
|
||||||
|
import BackgroundImageLoader from '@/components/BackgroundImageLoader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AboutPage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ContentPage,
|
||||||
|
BackgroundImageLoader,
|
||||||
|
},
|
||||||
|
|
||||||
|
head () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: 'All about the work of photographer Marc Leopold, his philosophy and inspirations.'
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData ({ $axios }) {
|
||||||
|
try {
|
||||||
|
const isNoContent = false
|
||||||
|
let { title, imageUrl, body } = await $axios.$get('/api/v1/about')
|
||||||
|
if (!body || body.length < 1) {
|
||||||
|
throw new Error('No body in response')
|
||||||
|
} else if (!imageUrl || imageUrl.length < 1) {
|
||||||
|
imageUrl = '/img/default-about.jpg'
|
||||||
|
}
|
||||||
|
return { title, imageUrl, body, isNoContent }
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: 'Coming Soon',
|
||||||
|
imageUrl: '/img/default-about.jpg',
|
||||||
|
body: `<div class="no-services">
|
||||||
|
<p class="no-conttent-text">Please check back, I will be updating this page shortly.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
isNoContent: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.about {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content {
|
||||||
|
color: $color__neutral-800;
|
||||||
|
text-shadow: 0 0 3px #000;
|
||||||
|
|
||||||
|
@media (min-width: 60em) {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ blockquote {
|
||||||
|
font-size: 1.4em;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ blockquote p {
|
||||||
|
color: $color__neutral-700;
|
||||||
|
text-shadow: 0 0 3px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: $color__neutral-900;
|
||||||
|
@include font-title(400);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-top {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ ul {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ li {
|
||||||
|
position: relative;
|
||||||
|
padding: 1.25rem 0 1.25rem;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2rem;
|
||||||
|
height: 1px;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
background-color: rgba($color__neutral-500, .6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content /deep/ .background-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
<template>
|
||||||
|
<ContentPage :heading="title">
|
||||||
|
|
||||||
|
<form class="contact-form">
|
||||||
|
<div class="form-background"></div>
|
||||||
|
<div class="form-content">
|
||||||
|
<b-field label="Name"
|
||||||
|
:type="attemptedSubmit && form.name.length < 1 ? 'is-danger' : ''"
|
||||||
|
:message="attemptedSubmit && form.name.length < 1 ? 'Please include your name.' : ''"
|
||||||
|
>
|
||||||
|
<b-input v-model.trim="form.name"
|
||||||
|
@input="$v.form.name.$touch()"
|
||||||
|
placeholder="Your name ...">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Email"
|
||||||
|
:type="attemptedSubmit && form.email.length < 1 ? 'is-danger' : ''"
|
||||||
|
:message="attemptedSubmit && form.email.length < 1 ? 'Please include your email address.' : ''"
|
||||||
|
>
|
||||||
|
<b-input v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Your email address ...">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Subject">
|
||||||
|
<b-input v-model="form.subject"
|
||||||
|
placeholder="Your subject ...">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Message"
|
||||||
|
:type="attemptedSubmit && form.message.length < 1 ? 'is-danger' : ''"
|
||||||
|
:message="attemptedSubmit && form.message.length < 1 ? 'Please include a message.' : ''"
|
||||||
|
>
|
||||||
|
<b-input v-model="form.message"
|
||||||
|
placeholder="Your message ..."
|
||||||
|
type="textarea">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div class="form-footer">
|
||||||
|
<button class="btn-submit"
|
||||||
|
type="button"
|
||||||
|
@click.prevent="onSubmit"
|
||||||
|
:disabled="isDisabled">
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="social-nav">
|
||||||
|
<li v-for="item in socialNav"
|
||||||
|
:key="item.to"
|
||||||
|
class="social-nav__item"
|
||||||
|
>
|
||||||
|
<a class="social-nav__link social-link"
|
||||||
|
:href="item.to"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<b-icon class="social-nav__icon"
|
||||||
|
:icon="item.icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<BackgroundImageLoader slot="background"
|
||||||
|
class="background"
|
||||||
|
:image-url="imageUrl"
|
||||||
|
>
|
||||||
|
<div slot="overlay" class="background-tint background-overlay"></div>
|
||||||
|
</BackgroundImageLoader>
|
||||||
|
</ContentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContentPage from '@/components/ContentPage'
|
||||||
|
import { required, email } from 'vuelidate/lib/validators'
|
||||||
|
import BackgroundImageLoader from '@/components/BackgroundImageLoader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContactPage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ContentPage,
|
||||||
|
BackgroundImageLoader,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
attemptedSubmit: false,
|
||||||
|
form: {
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
subject: "",
|
||||||
|
message: "",
|
||||||
|
},
|
||||||
|
socialNav: [
|
||||||
|
{ 'to': 'https://www.instagram.com', 'text': 'Instagram', icon: 'instagram' },
|
||||||
|
{ 'to': 'https://www.facebook.com', 'text': 'Facebook', icon: 'facebook' },
|
||||||
|
{ 'to': 'https://twitter.com', 'text': 'Twitter', icon: 'twitter' },
|
||||||
|
{ 'to': 'https://uk.linkedin.com', 'text': 'LinkedIn', icon: 'linkedin' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
head () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: 'Contact the photographer Marc Leopold with any queries for a prompt response.'
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isDisabled () {
|
||||||
|
return this.attemptedSubmit && this.$v.form.$invalid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validations: {
|
||||||
|
form: {
|
||||||
|
name: {
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
required,
|
||||||
|
email
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSubmit () {
|
||||||
|
this.attemptedSubmit = true
|
||||||
|
if (this.$v.form.$invalid) {
|
||||||
|
this.$toast.open({
|
||||||
|
message: 'Please correct errors before submitting.',
|
||||||
|
type: 'is-danger'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$toast.open('Submitting your message ...')
|
||||||
|
this.submitForm()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitForm() {
|
||||||
|
try {
|
||||||
|
await this.$axios({
|
||||||
|
method: 'post',
|
||||||
|
baseURL: 'http://192.168.0.5:3003',
|
||||||
|
url: '/api/contact',
|
||||||
|
proxy: false,
|
||||||
|
debug: true,
|
||||||
|
data: {
|
||||||
|
name: this.form.name,
|
||||||
|
email: this.form.email,
|
||||||
|
msg: this.form.message
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.$toast.open({
|
||||||
|
message: 'sorry, there was a problem submitting your message.',
|
||||||
|
type: 'is-danger'
|
||||||
|
})
|
||||||
|
console.error(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$toast.open({
|
||||||
|
message: 'Thank you, your message has been sent.',
|
||||||
|
type: 'is-success'
|
||||||
|
})
|
||||||
|
this.form.message = ''
|
||||||
|
this.form.subject = ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData ({ $axios }) {
|
||||||
|
try {
|
||||||
|
let { imageUrl, title } = await $axios.$get('/api/v1/page/about')
|
||||||
|
if (!imageUrl) {
|
||||||
|
throw new Error('empty imageUrl')
|
||||||
|
}
|
||||||
|
if (!title || title === '') {
|
||||||
|
title = 'Contact Me'
|
||||||
|
}
|
||||||
|
return { imageUrl, title }
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
imageUrl: '/img/default-contact.jpg',
|
||||||
|
title: 'Contact Me',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 1rem 2rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(#fff, .95);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom right,
|
||||||
|
rgba(#fff, .95),
|
||||||
|
rgba($color__neutral-800, .97)
|
||||||
|
),
|
||||||
|
url(/img/default-contact.jpg);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: top left;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
z-index: 5;
|
||||||
|
max-width: 30em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
font-size: 1.2em;
|
||||||
|
border: 2px solid $color__neutral-300;
|
||||||
|
padding: .2em .8em;
|
||||||
|
@include font-title(600);
|
||||||
|
color: $color__neutral-500;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: opacity .3s;
|
||||||
|
opacity: .7;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: .1;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 90%;
|
||||||
|
height: 1px;
|
||||||
|
top: 0;
|
||||||
|
left: 5%;
|
||||||
|
background-color: $color__neutral-800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-nav__item {
|
||||||
|
margin: 0 .2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
transition: opacity .3s;
|
||||||
|
|
||||||
|
&:link, &:visited {
|
||||||
|
color: $color__neutral-100;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<GalleryPage v-if="galleries && galleries.length > 0" :galleries="galleries">
|
||||||
|
</GalleryPage>
|
||||||
|
<NoContent v-else :heading="title"
|
||||||
|
message="My galleries are being prepared for upload, please check back soon."
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GalleryPage from '@/components/GalleryPage'
|
||||||
|
import NoContent from '@/components/NoContent'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GalleriesPage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
GalleryPage,
|
||||||
|
NoContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
galleries: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
head () {
|
||||||
|
return {
|
||||||
|
// title: 'My Galleries',
|
||||||
|
title: this.title,
|
||||||
|
meta: [{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: 'A showcase of the work of photographer Marc Leopold.'
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData ({ $axios }) {
|
||||||
|
try {
|
||||||
|
let { galleries, title } = await $axios.$get('/api/v1/galleries')
|
||||||
|
if (title === '') {
|
||||||
|
title = 'My Galleries'
|
||||||
|
}
|
||||||
|
return { galleries, title }
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
galleries: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,232 @@
|
||||||
|
<template>
|
||||||
|
<section class="home">
|
||||||
|
<div class="background background-img">
|
||||||
|
<BackgroundImageLoader slot="background"
|
||||||
|
:image-url="currentImageUrl"
|
||||||
|
@imageLoaded="handleImageLoaded"
|
||||||
|
@imageLoadError="handleImageLoadError"
|
||||||
|
/>
|
||||||
|
<div class="background background-overlay"></div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<transition name="fade">
|
||||||
|
<h1 v-if="showHeading" class="heading">Marc Leopold Photography</h1>
|
||||||
|
</transition>
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<p class="tagline"
|
||||||
|
:key="currentTaglineIndex">
|
||||||
|
{{ tagline }}
|
||||||
|
</p>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContentPage from '@/components/ContentPage'
|
||||||
|
import BackgroundImageLoader from '@/components/BackgroundImageLoader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HomePage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ContentPage,
|
||||||
|
BackgroundImageLoader
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showHeading: false,
|
||||||
|
currentImageIndex: -1,
|
||||||
|
currentTaglineIndex: -1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
currentImageUrl () {
|
||||||
|
let url = null
|
||||||
|
if (this.currentImageIndex > -1) {
|
||||||
|
url = this.bgImages[this.currentImageIndex]
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
},
|
||||||
|
|
||||||
|
tagline () {
|
||||||
|
return this.currentTaglineIndex > -1 ? this.taglines[this.currentTaglineIndex].text : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
head () {
|
||||||
|
return {
|
||||||
|
title: 'Home',
|
||||||
|
meta: [{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: 'Photographer, Marc Leopold has images in numerous collections and publications. Here is a glimpse of his work, an insight into his philosophy and motivations.'
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
if (this.bgImages.length > 0) {
|
||||||
|
this.setNextIndex()
|
||||||
|
this.showHeading = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData({ $axios }) {
|
||||||
|
try {
|
||||||
|
const { bgImages, taglines } = await $axios.$get('/api/v1/home')
|
||||||
|
if (bgImages.length < 1) {
|
||||||
|
throw new Error('bgImages empty')
|
||||||
|
}
|
||||||
|
return { bgImages, taglines }
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
bgImages: ['/img/default-home.jpg'],
|
||||||
|
taglines: [''],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setNextIndex () {
|
||||||
|
this.currentImageIndex =
|
||||||
|
this.currentImageIndex < this.bgImages.length - 1 ? this.currentImageIndex + 1 : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
nextTagline () {
|
||||||
|
this.currentTaglineIndex =
|
||||||
|
this.currentTaglineIndex < this.taglines.length - 1 ? this.currentTaglineIndex + 1 : 0
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageLoaded () {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.nextTagline()
|
||||||
|
}, 700)
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.setNextIndex()
|
||||||
|
}, 6000)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageLoadError () {
|
||||||
|
this.setNextIndex()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
$heading-height: 4em;
|
||||||
|
$tagline-height: 2.5em;
|
||||||
|
$padding: 1rem;
|
||||||
|
|
||||||
|
.home {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding: $site-menu__header-height + $padding $padding $padding;
|
||||||
|
|
||||||
|
font-size: .5rem;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: $padding $padding $padding $site-menu__header-width + $padding;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-img {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
$color: $color__neutral-100;
|
||||||
|
|
||||||
|
background-color: rgba($color, .4);
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom left,
|
||||||
|
rgba($color, .1),
|
||||||
|
rgba($color, .5)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba($color, .9),
|
||||||
|
rgba($color, .7) $heading-height * 1.5,
|
||||||
|
rgba($color, .4) $heading-height * 3,
|
||||||
|
rgba($color, 0) $heading-height * 6,
|
||||||
|
rgba($color, 0)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba($color, .6),
|
||||||
|
rgba($color, 0) $tagline-height * 4,
|
||||||
|
rgba($color, 0)
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color, .4),
|
||||||
|
rgba($color, 0) 20%,
|
||||||
|
rgba($color, 0) 80%,
|
||||||
|
rgba($color, .4)
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
at 100% 100%,
|
||||||
|
rgba($color, .4) 0,
|
||||||
|
rgba($color, 0) 40%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 80rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
padding: 1rem 0 0;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
padding: 2rem 3rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: $color__neutral-900;
|
||||||
|
font-size: $heading-height;
|
||||||
|
@include font-title(400);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: $color__neutral-600;
|
||||||
|
font-size: $tagline-height;
|
||||||
|
text-align: right;
|
||||||
|
@include font-cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade {
|
||||||
|
$timing: .7;
|
||||||
|
&-enter-active {
|
||||||
|
transition: opacity 5s * $timing .5s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-leave-active {
|
||||||
|
transition: opacity 2s * $timing ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter, &-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,271 @@
|
||||||
|
<template>
|
||||||
|
<ContentPage :heading="title">
|
||||||
|
<BackgroundImagePreloader v-if="showPreloaderBackground && backgroundImageUrls.length > 0"
|
||||||
|
slot="background"
|
||||||
|
class="background-preloader"
|
||||||
|
:image-urls="backgroundImageUrls"
|
||||||
|
:active-index="activeIndex"
|
||||||
|
>
|
||||||
|
<div slot="overlay" class="background-tint background-overlay"></div>
|
||||||
|
</BackgroundImagePreloader>
|
||||||
|
<BackgroundImageLoader v-else slot="background"
|
||||||
|
class="background"
|
||||||
|
:image-url="imageUrl"
|
||||||
|
>
|
||||||
|
<div slot="overlay" class="background-tint background-overlay"></div>
|
||||||
|
</BackgroundImageLoader>
|
||||||
|
|
||||||
|
<ul v-if="services.length > 0" class="services-list">
|
||||||
|
<li v-for="(service, index) in services"
|
||||||
|
:key="index"
|
||||||
|
class="services-list__item"
|
||||||
|
:style="{ 'z-index': services.length - index + 1}"
|
||||||
|
@mouseover="handleMouseOver(index)"
|
||||||
|
>
|
||||||
|
<div class="background background-image"
|
||||||
|
:style="{ 'background-image': `url(${service.imageUrl})` }"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="background background-overlay"></div>
|
||||||
|
<div class="services-list__content">
|
||||||
|
<h2 class="services-list__heading service-heading">{{ service.heading }}</h2>
|
||||||
|
<div class="services-list__body" v-html="service.html">
|
||||||
|
</div>
|
||||||
|
<a class="btn-link services-list__gallery-link" :href="service.linkUrl">My {{ service.heading }}</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="no-content-container">
|
||||||
|
<h2>Coming Soon</h2>
|
||||||
|
<p class="no-content-text">I will be updating soon, please check back for information on the services I provide.</p>
|
||||||
|
</div>
|
||||||
|
</ContentPage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContentPage from '@/components/ContentPage'
|
||||||
|
import BackgroundImagePreloader from '@/components/BackgroundImagePreloader'
|
||||||
|
import BackgroundImageLoader from '@/components/BackgroundImageLoader'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ServicesPage',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ContentPage,
|
||||||
|
BackgroundImagePreloader,
|
||||||
|
BackgroundImageLoader,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showPreloaderBackground: false,
|
||||||
|
activeIndex: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
head () {
|
||||||
|
return {
|
||||||
|
title: this.title,
|
||||||
|
meta: [{
|
||||||
|
hid: 'description',
|
||||||
|
name: 'description',
|
||||||
|
content: 'An overview of the services provided by the photographer marc Leopold.'
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
backgroundImageUrls () {
|
||||||
|
return this.services.map(el => el.backgroundImageUrl)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
const mq = window.matchMedia("(min-width: 40em)")
|
||||||
|
this.showPreloaderBackground = mq.matches
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handleMouseOver(index) {
|
||||||
|
this.activeIndex = index
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async asyncData ({ $axios }) {
|
||||||
|
try {
|
||||||
|
const { services } = await $axios.$get('api/v1/services')
|
||||||
|
let { imageUrl, title } = await $axios.$get('api/v1/page/services')
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
imageUrl = '/img/default-services.jpg'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title || title === '') {
|
||||||
|
title = 'My Services'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { services, imageUrl, title }
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
services: [],
|
||||||
|
imageUrl: '/img/default-services.jpg',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.services-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__item {
|
||||||
|
$min-height: 32rem;
|
||||||
|
$overlap: 4rem;
|
||||||
|
$padding-vertical: 2rem;
|
||||||
|
$color-overlay: $color__neutral-200;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
min-height: $min-height;
|
||||||
|
padding: $overlap + $padding-vertical 2rem $overlap;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: -1 * $overlap;
|
||||||
|
|
||||||
|
background-color: $color__neutral-200;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% calc(100% - #{$overlap}), 0 100%);
|
||||||
|
|
||||||
|
.background-overlay {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba($color-overlay, .1),
|
||||||
|
rgba($color-overlay, 1) 75%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__heading {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__body {
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - #{$overlap}));
|
||||||
|
.background-overlay {
|
||||||
|
background: linear-gradient(
|
||||||
|
to left,
|
||||||
|
rgba($color-overlay, .1),
|
||||||
|
rgba($color-overlay, 1) 75%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__gallery-link {
|
||||||
|
left: auto;
|
||||||
|
right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__body {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
min-height: $min-height - $overlap;
|
||||||
|
padding-top: $padding-vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: $padding-vertical;
|
||||||
|
clip-path: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__img {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
height: 20em;
|
||||||
|
width: auto;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
opacity: .4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 20em;
|
||||||
|
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__body {
|
||||||
|
width: 100%;
|
||||||
|
color: $color__neutral-800;
|
||||||
|
|
||||||
|
@media (min-width: $bp__layout) {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 70em) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-list__gallery-link {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3rem;
|
||||||
|
left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
// opacity: .3;
|
||||||
|
background-size: cover;
|
||||||
|
filter: grayscale(.2);
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-heading {
|
||||||
|
@include font-title();
|
||||||
|
color: $color__neutral-900;
|
||||||
|
font-size: 2.0em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# PLUGINS
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Javascript plugins that you want to run before mounting the root Vue.js application.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins).
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuelidate from 'vuelidate'
|
||||||
|
Vue.use(Vuelidate)
|
|
@ -0,0 +1,10 @@
|
||||||
|
# STATIC
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your static files.
|
||||||
|
Each file inside this directory is mapped to `/`.
|
||||||
|
|
||||||
|
Example: `/static/robots.txt` is mapped as `/robots.txt`.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static).
|
After Width: | Height: | Size: 515 B |
After Width: | Height: | Size: 720 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 515 B |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 677 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 163 KiB |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 178 KiB |
After Width: | Height: | Size: 648 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 115 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
# STORE
|
||||||
|
|
||||||
|
**This directory is not required, you can delete it if you don't want to use it.**
|
||||||
|
|
||||||
|
This directory contains your Vuex Store files.
|
||||||
|
Vuex Store option is implemented in the Nuxt.js framework.
|
||||||
|
|
||||||
|
Creating a file in this directory activate the option in the framework automatically.
|
||||||
|
|
||||||
|
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
|
|
@ -0,0 +1,29 @@
|
||||||
|
export const state = () => ({})
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async nuxtServerInit ({ commit }, { $axios }) {
|
||||||
|
try {
|
||||||
|
const { siteNav, socialNav } = await $axios.$get('/api/v1/navigation')
|
||||||
|
if (siteNav.length < 1) {
|
||||||
|
throw new Error('siteNav empty')
|
||||||
|
}
|
||||||
|
commit('navigation/updateSiteNav', siteNav)
|
||||||
|
commit('navigation/updateSocialNav', socialNav)
|
||||||
|
} catch {
|
||||||
|
commit('navigation/updateSiteNav', [
|
||||||
|
{ "to": "/", "text": "Home", "bgImgUrl": "/img/devices--bw.jpg"},
|
||||||
|
{ "to": "/galleries", "text": "Galleries", "bgImgUrl": "/img/photo-box--bw.jpg" },
|
||||||
|
{ "to": "/services", "text": "Services", "bgImgUrl": "/img/camera--bw.jpg" },
|
||||||
|
{ "to": "/about", "text": "About Me", "bgImgUrl": "/img/silhouette--dark.jpg" },
|
||||||
|
{ "to": "/contact", "text": "Contact Me", "bgImgUrl": "/img/mail--bw.jpg" }
|
||||||
|
])
|
||||||
|
commit('navigation/updateSocialNav', [
|
||||||
|
{ "to": "https://www.instagram.com", "text": "Instagram", "icon": "instagram" },
|
||||||
|
{ "to": "https://www.facebook.com", "text": "Facebook", "icon": "facebook" },
|
||||||
|
{ "to": "https://twitter.com", "text": "Twitter", "icon": "twitter" },
|
||||||
|
{ "to": "https://uk.linkedin.com", "text": "LinkedIn", "icon": "linkedin" }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
export const state = () => ({
|
||||||
|
siteNav: [],
|
||||||
|
socialNav: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
siteNav: state => {
|
||||||
|
return state.siteNav
|
||||||
|
},
|
||||||
|
|
||||||
|
socialNav: state => {
|
||||||
|
return state.socialNav
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
updateSiteNav (state, navItems) {
|
||||||
|
state.siteNav = navItems
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSocialNav (state, socialItems) {
|
||||||
|
state.socialNav = socialItems
|
||||||
|
},
|
||||||
|
}
|