folder structure

This commit is contained in:
ManjaroOne666 2019-02-24 16:42:27 +00:00
parent 1e53c9ea47
commit 106f7491d1
65 changed files with 12873 additions and 0 deletions

13
app/.editorconfig Normal file
View File

@ -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

25
app/.eslintrc.js Normal file
View File

@ -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'
}
}

22
app/README.md Normal file
View File

@ -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).

63
app/api/contact.js Normal file
View File

@ -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)
}

7
app/assets/README.md Normal file
View File

@ -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).

View File

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

View File

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

View File

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

View File

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

View File

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

194
app/assets/scss/style.scss Normal file
View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

79
app/components/Logo.vue Normal file
View File

@ -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>

View File

@ -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>

7
app/components/README.md Normal file
View File

@ -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._

629
app/components/SiteMenu.vue Normal file
View File

@ -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 &copy; {{ 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>

View File

@ -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>

7
app/layouts/README.md Normal file
View File

@ -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).

97
app/layouts/default.vue Normal file
View File

@ -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>

73
app/layouts/error.vue Normal file
View File

@ -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>

8
app/middleware/README.md Normal file
View File

@ -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).

16
app/mixins/imageLoader.js Normal file
View File

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

131
app/nuxt.config.js Normal file
View File

@ -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)/
})
}
}
}
}

38
app/package.json Normal file
View File

@ -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"
}
}

6
app/pages/README.md Normal file
View File

@ -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).

152
app/pages/about.vue Normal file
View File

@ -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>

322
app/pages/contact.vue Normal file
View File

@ -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>

53
app/pages/galleries.vue Normal file
View File

@ -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>

232
app/pages/index.vue Normal file
View File

@ -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>

271
app/pages/services.vue Normal file
View File

@ -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>

7
app/plugins/README.md Normal file
View File

@ -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).

3
app/plugins/Vuelidate.js Normal file
View File

@ -0,0 +1,3 @@
import Vue from 'vue'
import Vuelidate from 'vuelidate'
Vue.use(Vuelidate)

10
app/static/README.md Normal file
View File

@ -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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

BIN
app/static/img/camera--bw.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
app/static/img/camera.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

BIN
app/static/img/contact-form.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
app/static/img/default-about.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
app/static/img/devices--bw.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
app/static/img/mail--bw.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
app/static/img/mail.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

BIN
app/static/img/photo-box--bw.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

10
app/store/README.md Normal file
View File

@ -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).

29
app/store/index.js Normal file
View File

@ -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" }
])
}
}
}

24
app/store/navigation.js Normal file
View File

@ -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
},
}

8378
app/yarn.lock Normal file

File diff suppressed because it is too large Load Diff