Initial commit
This commit is contained in:
commit
affcc00ba8
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
|
||||||
92
.gitignore
vendored
Normal file
92
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Node template
|
||||||
|
# Logs
|
||||||
|
/logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# Nuxt generate
|
||||||
|
dist
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Service worker
|
||||||
|
sw.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
||||||
|
/backend/env/
|
||||||
|
/yarn.lock
|
||||||
2
.nuxtignore
Normal file
2
.nuxtignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pages/projects/*.vue
|
||||||
|
components/projects/*.vue
|
||||||
7
README.md
Normal file
7
README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Useful commands
|
||||||
|
### Create thumbnail (400px side max) for all jpg/JPG images in folder, move it to `thumbs` folder
|
||||||
|
|
||||||
|
```for ext in jpg JPG; do mogrify -format $ext -path thumbs -auto-orient -thumbnail '400' *.$ext; done```
|
||||||
|
|
||||||
|
### Get thumbnail size for every file in folder
|
||||||
|
```identify -ping -format 'image: "%[basename].%[extension]", thumbSize: {w: %[width], h: %[height]}},\n' *```
|
||||||
30
app/router.scrollBehavior.js
Normal file
30
app/router.scrollBehavior.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export default async function (to, from, savedPosition) {
|
||||||
|
if (savedPosition) {
|
||||||
|
return savedPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const findEl = async (hash, x = 0) => {
|
||||||
|
return (
|
||||||
|
document.querySelector(hash) ||
|
||||||
|
new Promise(resolve => {
|
||||||
|
if (x > 50) {
|
||||||
|
return resolve(document.querySelector("#app"));
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(findEl(hash, ++x || 1));
|
||||||
|
}, 100);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (to.hash) {
|
||||||
|
let el = await findEl(to.hash);
|
||||||
|
if ("scrollBehavior" in document.documentElement.style) {
|
||||||
|
return window.scrollTo({top: el.offsetTop, behavior: "smooth"});
|
||||||
|
} else {
|
||||||
|
return window.scrollTo(0, el.offsetTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {x: 0, y: 0};
|
||||||
|
}
|
||||||
0
assets/app.css
Normal file
0
assets/app.css
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Bold.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BoldObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Book.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Book.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Book.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Book.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Book.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Book.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-BookObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Demi.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-DemiObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-ExtraBoldObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Heavy.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-HeavyObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Light.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Light.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Light.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Light.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Light.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-LightObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-Medium.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPT-MediumObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Bold.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BoldObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Book.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-BookObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-ExtraBoldObl.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-Medium.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.ttf
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.woff
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.woff
Normal file
Binary file not shown.
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.woff2
Normal file
BIN
assets/fonts/FuturaPT/FuturaPTCond-MediumObl.woff2
Normal file
Binary file not shown.
249
assets/fonts/FuturaPT/futura-pt.css
Normal file
249
assets/fonts/FuturaPT/futura-pt.css
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
/* === Font-faces for FuturaPT === */
|
||||||
|
|
||||||
|
/* Light regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Light';
|
||||||
|
src: url('FuturaPT-Light.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Light.woff') format('woff'),
|
||||||
|
url('FuturaPT-Light.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-LightItalic';
|
||||||
|
src: url('FuturaPT-LightObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-LightObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-LightObl.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Regular';
|
||||||
|
src: url('FuturaPT-Book.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Book.woff') format('woff'),
|
||||||
|
url('FuturaPT-Book.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Regular italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Italic';
|
||||||
|
src: url('FuturaPT-BookObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-BookObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-BookObl.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Medium';
|
||||||
|
src: url('FuturaPT-Medium.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Medium.woff') format('woff'),
|
||||||
|
url('FuturaPT-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Medium italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-MediumItalic';
|
||||||
|
src: url('FuturaPT-MediumObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-MediumObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-MediumObl.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Semibold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-SemiBold';
|
||||||
|
src: url('FuturaPT-Demi.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Demi.woff') format('woff'),
|
||||||
|
url('FuturaPT-Demi.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Semibold italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-SemiBoldItalic';
|
||||||
|
src: url('FuturaPT-DemiObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-DemiObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-DemiObl.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Bold';
|
||||||
|
src: url('FuturaPT-Bold.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Bold.woff') format('woff'),
|
||||||
|
url('FuturaPT-Bold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Bold italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-BoldItalic';
|
||||||
|
src: url('FuturaPT-BoldObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-BoldObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-BoldObl.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heavy */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Heavy';
|
||||||
|
src: url('FuturaPT-Heavy.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-Heavy.woff') format('woff'),
|
||||||
|
url('FuturaPT-Heavy.ttf') format('truetype');
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heavy italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-HeavyItalic';
|
||||||
|
src: url('FuturaPT-HeavyObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-HeavyObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-HeavyObl.ttf') format('truetype');
|
||||||
|
font-weight: 900;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-ExtraBold';
|
||||||
|
src: url('FuturaPT-ExtraBold.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-ExtraBold.woff') format('woff'),
|
||||||
|
url('FuturaPT-ExtraBold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra bold italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-ExtraBoldItalic';
|
||||||
|
src: url('FuturaPT-ExtraBoldObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPT-ExtraBoldObl.woff') format('woff'),
|
||||||
|
url('FuturaPT-ExtraBoldObl.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Futura PT Condensed === */
|
||||||
|
/* Condensed regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-Regular';
|
||||||
|
src: url('FuturaPTCond-Book.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-Book.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-Book.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-Italic';
|
||||||
|
src: url('FuturaPTCond-BookObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-BookObl.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-BookObl.ttf') format('truetype');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed bold italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-BoldItalic';
|
||||||
|
src: url('FuturaPTCond-BoldObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-BoldObl.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-BoldObl.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed medium */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-Medium';
|
||||||
|
src: url('FuturaPTCond-Medium.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-Medium.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed medium italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-MediumItalic';
|
||||||
|
src: url('FuturaPTCond-MediumObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-MediumObl.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-MediumObl.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed medium bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPTCondensed-Bold';
|
||||||
|
src: url('FuturaPTCond-Bold.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-Bold.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-Bold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed extra bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-ExtraBold';
|
||||||
|
src: url('FuturaPTCond-ExtraBold.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-ExtraBold.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-ExtraBold.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Condensed extra bold italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'FuturaPT-Condensed-ExtraBoldItalic';
|
||||||
|
src: url('FuturaPTCond-ExtraBoldObl.woff2') format('woff2'),
|
||||||
|
url('FuturaPTCond-ExtraBoldObl.woff') format('woff'),
|
||||||
|
url('FuturaPTCond-ExtraBoldObl.ttf') format('truetype');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* === CSS === */
|
||||||
|
|
||||||
|
.font-general-light {
|
||||||
|
font-family: "FuturaPT-Light";
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-general-regular {
|
||||||
|
font-family: "FuturaPT-Regular";
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-general-medium {
|
||||||
|
font-family: "FuturaPT-Medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-general-semibold {
|
||||||
|
font-family: "FuturaPT-SemiBold";
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-general-bold {
|
||||||
|
font-family: "FuturaPT-Bold";
|
||||||
|
}
|
||||||
21
backend/config.py
Normal file
21
backend/config.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
# TODO: store keys in env variables
|
||||||
|
RECAPTCHA_PUBLIC_KEY = "RECAPTCHA_PUBLIC_KEY"
|
||||||
|
RECAPTCHA_PRIVATE_KEY = "RECAPTCHA_PRIVATE_KEY-L4DsNDlza17N7dEz36"
|
||||||
|
|
||||||
|
FLASK_APIKEY = "FLASK_APIKEY"
|
||||||
|
|
||||||
|
if 'ALEX_DEBUG' in os.environ:
|
||||||
|
DEPLOY_PATH = '.'
|
||||||
|
else:
|
||||||
|
DEPLOY_PATH = '/home/c/cn52774/alex-sharoff/public_html/backend'
|
||||||
|
|
||||||
|
CONTACT_REQUEST_TEMPLATE = """
|
||||||
|
Обращение через форму обратной связи с сайта alex-sharoff.ru:
|
||||||
|
|
||||||
|
Имя: {name}
|
||||||
|
Почта: {email}
|
||||||
|
-------
|
||||||
|
{message}
|
||||||
|
"""
|
||||||
9
backend/index.wsgi
Normal file
9
backend/index.wsgi
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/home/c/cn52774/alex-sharoff/public_html/backend/env/bin/python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append('/home/c/cn52774/alex-sharoff/public_html/backend/')
|
||||||
|
sys.path.append('/home/c/cn52774/alex-sharoff/public_html/backend/env/lib/python3.6/site-packages/')
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
application = app
|
||||||
74
backend/main.py
Normal file
74
backend/main.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import locale
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import Flask, request
|
||||||
|
from flask_cors import CORS, cross_origin
|
||||||
|
from flask_executor import Executor
|
||||||
|
from flask_wtf import FlaskForm, RecaptchaField
|
||||||
|
import telegram_send
|
||||||
|
from wtforms import StringField, EmailField
|
||||||
|
from wtforms.validators import DataRequired, Email
|
||||||
|
|
||||||
|
import config as cfg
|
||||||
|
|
||||||
|
# Set locale to UTF-8 as server's one is ANSI - this breaks cyrilic loggng
|
||||||
|
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||||
|
|
||||||
|
logging.basicConfig(filename=f"{cfg.DEPLOY_PATH}/log.log",
|
||||||
|
filemode='a',
|
||||||
|
format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s',
|
||||||
|
datefmt='%H:%M:%S',
|
||||||
|
level=logging.INFO)
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.secret_key = cfg.FLASK_APIKEY
|
||||||
|
app.config["APPLICATION_ROOT"] = "/api"
|
||||||
|
app.config["WTF_CSRF_ENABLED"] = False
|
||||||
|
|
||||||
|
app.config["RECAPTCHA_PUBLIC_KEY"] = cfg.RECAPTCHA_PUBLIC_KEY
|
||||||
|
app.config["RECAPTCHA_PRIVATE_KEY"] = cfg.RECAPTCHA_PRIVATE_KEY
|
||||||
|
|
||||||
|
app.config['EXECUTOR_TYPE'] = 'process'
|
||||||
|
app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True
|
||||||
|
|
||||||
|
executor = Executor(app)
|
||||||
|
|
||||||
|
|
||||||
|
def send_telegram(message, silent=False):
|
||||||
|
telegram_send.send(messages=[message], conf=f'{cfg.DEPLOY_PATH}/telegram.conf', silent=silent)
|
||||||
|
|
||||||
|
|
||||||
|
class ContactForm(FlaskForm):
|
||||||
|
name = StringField('Username', validators=[DataRequired()])
|
||||||
|
email = EmailField('Email', validators=[DataRequired(), Email()])
|
||||||
|
message = StringField('Message')
|
||||||
|
recaptcha = RecaptchaField()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/contact/', methods=['POST'])
|
||||||
|
def contact():
|
||||||
|
form = ContactForm(request.form)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
text = cfg.CONTACT_REQUEST_TEMPLATE.format(
|
||||||
|
name=form.name.data,
|
||||||
|
email=form.email.data,
|
||||||
|
message=form.message.data
|
||||||
|
)
|
||||||
|
logger.info(f'{form.name.data} - {form.email.data}: [{form.message.data}]')
|
||||||
|
executor.submit(send_telegram, text)
|
||||||
|
return '', 200
|
||||||
|
|
||||||
|
return '', 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def hello_world():
|
||||||
|
return 'Hello, World!'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
8
backend/telegram.conf
Normal file
8
backend/telegram.conf
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[telegram]
|
||||||
|
# alexsharoff_bot
|
||||||
|
# token = token
|
||||||
|
# chat_id = chat_id
|
||||||
|
|
||||||
|
# phzhik_dev_bot
|
||||||
|
token = token
|
||||||
|
chat_id = chat_id
|
||||||
32
components/RoundButton.vue
Normal file
32
components/RoundButton.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-md
|
||||||
|
rounded-full
|
||||||
|
border-2
|
||||||
|
px-4
|
||||||
|
py-2
|
||||||
|
bg-white
|
||||||
|
hover:bg-background-dark
|
||||||
|
text-background-dark
|
||||||
|
hover:text-white
|
||||||
|
|
||||||
|
transition-colors duration-400
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "RoundButton",
|
||||||
|
props: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
83
components/SideBar.vue
Normal file
83
components/SideBar.vue
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script>
|
||||||
|
import feather from "feather-icons";
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
import SocialButton from "@/components/SocialButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {SocialButton},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
userScrollPosition: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["socialProfiles"]),
|
||||||
|
|
||||||
|
isScrolled() {
|
||||||
|
return this.userScrollPosition > 200;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("scroll", this.updateScrollPosition);
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener("scroll", this.updateScrollPosition);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateScrollPosition() {
|
||||||
|
this.userScrollPosition = window.scrollY;
|
||||||
|
},
|
||||||
|
|
||||||
|
backToTop() {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-show="isScrolled"
|
||||||
|
class="
|
||||||
|
flex
|
||||||
|
flex-col
|
||||||
|
sm:space-y-2
|
||||||
|
mr-4
|
||||||
|
sm:mr-8
|
||||||
|
xl:mr-16
|
||||||
|
mb-6
|
||||||
|
right-0
|
||||||
|
bottom-0
|
||||||
|
z-50
|
||||||
|
fixed
|
||||||
|
items-center
|
||||||
|
">
|
||||||
|
<SocialButton class="mb-4" feather-icon="chevron-up" @click.native="backToTop()"/>
|
||||||
|
<SocialButton v-for="social in socialProfiles"
|
||||||
|
:key="social.id"
|
||||||
|
:feather-icon="social.icon"
|
||||||
|
:url="social.url"
|
||||||
|
class="hidden lg:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
40
components/SocialButton.vue
Normal file
40
components/SocialButton.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<a :href="url"
|
||||||
|
target="_blank"
|
||||||
|
class="
|
||||||
|
transform
|
||||||
|
hover:scale-110
|
||||||
|
cursor-pointer
|
||||||
|
p-2
|
||||||
|
text-white
|
||||||
|
items-center
|
||||||
|
|
||||||
|
shadow-none
|
||||||
|
border-none
|
||||||
|
ring-none
|
||||||
|
outline-none
|
||||||
|
">
|
||||||
|
<i class="w-7 h-7" :data-feather="featherIcon"/>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SocialButton",
|
||||||
|
props: ["featherIcon", "url"],
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
25
components/about/AboutClientSingle.vue
Normal file
25
components/about/AboutClientSingle.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["client"],
|
||||||
|
data: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="shadow-sm border border-ternary-light rounded-lg bg-white">
|
||||||
|
<a :href="client.url" class="h-full" target="_blank">
|
||||||
|
<img
|
||||||
|
:src="client.img"
|
||||||
|
:alt="client.title"
|
||||||
|
class="h-20 md:h-28 max-h-28 md:max-h-32 p-4 sm:p-4 flex-shrink-0 flex-none"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
36
components/about/AboutClients.vue
Normal file
36
components/about/AboutClients.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
import { mapState } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["clientsHeading", "clients"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- About clients section -->
|
||||||
|
<div class="mt-10 sm:mt-20">
|
||||||
|
<p
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-2xl text-center
|
||||||
|
sm:text-3xl
|
||||||
|
text-white
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ clientsHeading }}
|
||||||
|
</p>
|
||||||
|
<div id="clients-list" class="flex flex-row flex-wrap mt-10 sm:mt-14 gap-4 mx-4 sm:mx-12 justify-center">
|
||||||
|
<AboutClientSingle
|
||||||
|
v-for="client in clients"
|
||||||
|
:key="client.id"
|
||||||
|
:client="client"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
100
components/about/AboutCounter.vue
Normal file
100
components/about/AboutCounter.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-10 sm:mt-20 bg-primary-light dark:bg-ternary-dark shadow-sm">
|
||||||
|
<!-- About me counters start -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
font-general-regular
|
||||||
|
container
|
||||||
|
mx-auto
|
||||||
|
py-20
|
||||||
|
block
|
||||||
|
sm:flex sm:justify-between sm:items-center
|
||||||
|
text-center
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- Years of experience counter -->
|
||||||
|
<div class="mb-20 sm:mb-0">
|
||||||
|
<span
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-4xl
|
||||||
|
font-general-bold
|
||||||
|
text-secondary-dark
|
||||||
|
dark:text-secondary-light
|
||||||
|
mb-2
|
||||||
|
"
|
||||||
|
>7+</span
|
||||||
|
>
|
||||||
|
<span class="block text-md text-ternary-dark dark:text-ternary-light"
|
||||||
|
>Years of experience</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GitHub stars counter -->
|
||||||
|
<div class="mb-20 sm:mb-0">
|
||||||
|
<span
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-4xl
|
||||||
|
font-general-bold
|
||||||
|
text-secondary-dark
|
||||||
|
dark:text-secondary-light
|
||||||
|
mb-2
|
||||||
|
"
|
||||||
|
>2k</span
|
||||||
|
>
|
||||||
|
<span class="block text-md text-ternary-dark dark:text-ternary-light"
|
||||||
|
>Stars on GitHub</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Positive feedback counter -->
|
||||||
|
<div class="mb-20 sm:mb-0">
|
||||||
|
<span
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-4xl
|
||||||
|
font-general-bold
|
||||||
|
text-secondary-dark
|
||||||
|
dark:text-secondary-light
|
||||||
|
mb-2
|
||||||
|
"
|
||||||
|
>32</span
|
||||||
|
>
|
||||||
|
<span class="block text-md text-ternary-dark dark:text-ternary-light"
|
||||||
|
>Positive feedback</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Projects completed counter -->
|
||||||
|
<div class="mb-20 sm:mb-0">
|
||||||
|
<span
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-4xl
|
||||||
|
font-general-bold
|
||||||
|
text-secondary-dark
|
||||||
|
dark:text-secondary-light
|
||||||
|
mb-2
|
||||||
|
"
|
||||||
|
>77</span
|
||||||
|
>
|
||||||
|
<span class="block text-md text-ternary-dark dark:text-ternary-light"
|
||||||
|
>Projects completed</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
40
components/about/AboutMe.vue
Normal file
40
components/about/AboutMe.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script>
|
||||||
|
import { mapState } from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(["aboutMe"]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="block sm:flex sm:gap-6 mt-10 sm:mt-20">
|
||||||
|
<!-- About profile image -->
|
||||||
|
<div class="md:w-2/4 mb-6">
|
||||||
|
<img src="/profile.jpg" class="rounded-lg w-96" alt="Profile pic" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- About details -->
|
||||||
|
<div class="w-full sm:w-2/4 text-left">
|
||||||
|
|
||||||
|
<p
|
||||||
|
v-for="(bio, index) in aboutMe"
|
||||||
|
:key="index"
|
||||||
|
class="
|
||||||
|
font-general-regular
|
||||||
|
clear-left
|
||||||
|
mb-4
|
||||||
|
text-white
|
||||||
|
text-xl
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ bio }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
47
components/contact/ContactDetails.vue
Normal file
47
components/contact/ContactDetails.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["contacts"],
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-left max-w-xl px-6">
|
||||||
|
<h2
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-2xl text-white
|
||||||
|
mt-8
|
||||||
|
mb-8
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Контакты
|
||||||
|
</h2>
|
||||||
|
<ul class="font-general-regular">
|
||||||
|
<li class="flex" v-for="contact in contacts" :key="contact.id">
|
||||||
|
<i
|
||||||
|
:data-feather="contact.icon"
|
||||||
|
class="w-5 text-gray-500 mr-4"
|
||||||
|
></i>
|
||||||
|
<a
|
||||||
|
:href="contact.url"
|
||||||
|
class="text-lg mb-4 text-white"
|
||||||
|
:class="
|
||||||
|
contact.icon === 'mail' || contact.icon === 'phone'
|
||||||
|
? 'hover:underline cursor-pointer'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
aria-label="Website and Phone"
|
||||||
|
>
|
||||||
|
{{ contact.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
190
components/contact/ContactForm.vue
Normal file
190
components/contact/ContactForm.vue
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
endpoint: process.env.NODE_ENV === 'production'
|
||||||
|
? '/api/contact'
|
||||||
|
: 'http://127.0.0.1:3001/contact',
|
||||||
|
|
||||||
|
labelStyle: "block text-lg text-primary-dark mb-2",
|
||||||
|
inputStyle: "w-full px-5 py-2 " +
|
||||||
|
"border border-gray-300 " +
|
||||||
|
"text-primary-dark bg-ternary-light " +
|
||||||
|
"rounded-md shadow-sm text-md " +
|
||||||
|
"focus:border-primary-dark focus:ring-0",
|
||||||
|
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
message: "",
|
||||||
|
|
||||||
|
captchaSolved: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
formReady() {
|
||||||
|
let isNotEmpty = this.name !== '' && this.email !== ''
|
||||||
|
let isValid = this.$refs.name.checkValidity() && this.$refs.email.checkValidity()
|
||||||
|
return this.captchaSolved && isNotEmpty && isValid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.$recaptcha.init()
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$recaptcha.destroy()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async resetCaptcha() {
|
||||||
|
await this.$recaptcha.reset()
|
||||||
|
this.captchaSolved = false
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSubmit() {
|
||||||
|
// To prevent `:invalid` CSS class to be applied on page load
|
||||||
|
this.$refs.form.classList.add("submitted")
|
||||||
|
|
||||||
|
console.log(this.endpoint)
|
||||||
|
|
||||||
|
if (!this.formReady) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append('name', this.name)
|
||||||
|
formData.append('email', this.email)
|
||||||
|
formData.append('message', this.message)
|
||||||
|
formData.append('g-recaptcha-response', await this.$recaptcha.getResponse())
|
||||||
|
|
||||||
|
fetch(this.endpoint,
|
||||||
|
{
|
||||||
|
method: "post",
|
||||||
|
mode: "cors",
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(response);
|
||||||
|
})
|
||||||
|
.then(_ => {
|
||||||
|
alert("Спасибо за обращение!")
|
||||||
|
this.resetCaptcha()
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert("Произошла ошибка, повторите отправку формы позднее")
|
||||||
|
console.error('Error:', error);
|
||||||
|
this.resetCaptcha()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
leading-loose
|
||||||
|
max-w-xl
|
||||||
|
p-7
|
||||||
|
bg-secondary-light
|
||||||
|
|
||||||
|
rounded-xl
|
||||||
|
shadow-xl
|
||||||
|
text-left
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-primary-dark
|
||||||
|
|
||||||
|
text-2xl
|
||||||
|
mb-8
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Свяжитесь со мной
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="font-general-regular space-y-7"
|
||||||
|
ref="form"
|
||||||
|
novalidate
|
||||||
|
>
|
||||||
|
<div class="">
|
||||||
|
<label :class="labelStyle" for="name">Ваше имя*</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="name"
|
||||||
|
v-model="name"
|
||||||
|
:class="inputStyle"
|
||||||
|
type="text"
|
||||||
|
placeholder=""
|
||||||
|
aria-label="Name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label :class="labelStyle" for="email">Электронная почта*</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref="email"
|
||||||
|
v-model="email"
|
||||||
|
:class="inputStyle"
|
||||||
|
type="email"
|
||||||
|
pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+$"
|
||||||
|
placeholder=""
|
||||||
|
aria-label="Email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label :class="labelStyle" for="message">Сообщение</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
ref="message"
|
||||||
|
:class="inputStyle"
|
||||||
|
v-model="message"
|
||||||
|
cols="14"
|
||||||
|
rows="6"
|
||||||
|
aria-label="Message"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<recaptcha
|
||||||
|
@error="captchaSolved = false"
|
||||||
|
@expired="captchaSolved = false"
|
||||||
|
@success="captchaSolved = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<RoundButton
|
||||||
|
@click.native.prevent="onSubmit()"
|
||||||
|
class="px-6 inline-flex items-center"
|
||||||
|
aria-label="Send Message"
|
||||||
|
>
|
||||||
|
Отправить
|
||||||
|
</RoundButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
form.submitted input:invalid {
|
||||||
|
@apply invalid:border-red-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
components/gallery/FilterGroup.vue
Normal file
50
components/gallery/FilterGroup.vue
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import FilterOption from "@/components/gallery/FilterOption.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {FilterOption},
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: []
|
||||||
|
},
|
||||||
|
initialOption: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
selectedOption: this.initialOption,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selectedOption: function () {
|
||||||
|
this.$emit('change', this.selectedOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <ul id="gallery-filter" class="flex flex-row flex-wrap text-left mt-50">-->
|
||||||
|
<ul id="gallery-filter"
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 md:flex md:flex-wrap gap-y-2.5 gap-x-6
|
||||||
|
text-left mt-50">
|
||||||
|
<!-- class="flex-auto mr-2.5 mb-2.5 text-white font-general-regular text-xl cursor-pointer select-none"-->
|
||||||
|
<FilterOption
|
||||||
|
class="text-white font-general-light text-xl lg:text-2xl cursor-pointer select-none tracking-wide"
|
||||||
|
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
:name="option.name"
|
||||||
|
:value="option.value"
|
||||||
|
:isActive="selectedOption === option.value"
|
||||||
|
@change="selectedOption = $event"/>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
35
components/gallery/FilterOption.vue
Normal file
35
components/gallery/FilterOption.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<!-- <li class="flex-auto rounded-md border-2 px-1 py-1.5 mr-2.5 mb-2.5 font-general-medium text-base sm:text-lg cursor-pointer select-none"-->
|
||||||
|
<li :class="[isActive ? activeClass : inactiveClass, hoverClass]"
|
||||||
|
:data-filter="`.${value}`"
|
||||||
|
@click="$emit('change', value)"
|
||||||
|
>
|
||||||
|
{{ name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "FilterOption",
|
||||||
|
props: {
|
||||||
|
name: {type: String, required: true},
|
||||||
|
value: {type: String, required: true},
|
||||||
|
isActive: {type: Boolean, default: false},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
inactiveClass: "",
|
||||||
|
activeClass: "underline underline-offset-8",
|
||||||
|
hoverClass: "hover:underline hover:underline-offset-8",
|
||||||
|
// inactiveClass: "border-gray-700 text-gray-700",
|
||||||
|
// activeClass: "bg-secondary-dark border-secondary-dark text-white",
|
||||||
|
// hoverClass: "hover:bg-ternary-dark hover:bg-ternary-dark hover:text-white",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
ul li {
|
||||||
|
transition: all .5s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
168
components/gallery/Gallery.vue
Normal file
168
components/gallery/Gallery.vue
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
<template>
|
||||||
|
<section class="transition-opacity duration-500 ease-in-out opacity-0"
|
||||||
|
:class="[galleryLoading ? 'opacity-0' : 'opacity-100']">
|
||||||
|
|
||||||
|
<FilterGroup
|
||||||
|
:options="categories"
|
||||||
|
:initial-option="initialCategory"
|
||||||
|
@change="selectedCategory = $event"
|
||||||
|
v-if="filterEnabled"/>
|
||||||
|
|
||||||
|
<div id='gallery' class='columns-auto min-h-screen items-center mt-2 sm:mt-5'>
|
||||||
|
<GalleryItem
|
||||||
|
v-for='img in filteredPhotos'
|
||||||
|
:key='img.id'
|
||||||
|
:category='img.category'
|
||||||
|
:image='img.image'
|
||||||
|
:thumb-size='img.thumbSize'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import lightGallery from 'lightgallery';
|
||||||
|
import lgThumbnail from 'lightgallery/plugins/thumbnail';
|
||||||
|
import lgZoom from 'lightgallery/plugins/zoom';
|
||||||
|
import lgFullScreen from 'lightgallery/plugins/fullscreen';
|
||||||
|
|
||||||
|
import ImagesLoaded from "imagesloaded/imagesloaded.js";
|
||||||
|
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
import FilterGroup from "@/components/gallery/FilterGroup.vue";
|
||||||
|
|
||||||
|
import Shuffle from 'shufflejs'
|
||||||
|
|
||||||
|
// let ScrollReveal;
|
||||||
|
// if (process.browser) {
|
||||||
|
// ScrollReveal = require('scrollreveal/dist/scrollreveal')
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Gallery',
|
||||||
|
components: {FilterGroup},
|
||||||
|
props: {
|
||||||
|
initialCategory: {type: String},
|
||||||
|
images: {type: Array, default: []},
|
||||||
|
filterEnabled: {type: Boolean, default: false},
|
||||||
|
allCategoryEnabled: {type: Boolean, default: false},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.ALL_CATEGORY = Shuffle.ALL_ITEMS;
|
||||||
|
|
||||||
|
// Object storage
|
||||||
|
this.shuffle = null
|
||||||
|
this.lg = null
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
selectedCategory: this.initialCategory,
|
||||||
|
galleryLoading: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["categoriesNames"]),
|
||||||
|
|
||||||
|
categories() {
|
||||||
|
// Go through all images, collect unique categories keys
|
||||||
|
let categories = [...new Set(this.images.map((img) => img.category))]
|
||||||
|
if (this.allCategoryEnabled) {
|
||||||
|
categories = [this.ALL_CATEGORY, ...categories]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for GalleryFilter: category key & human-readable name
|
||||||
|
return categories.map(c => {
|
||||||
|
return {
|
||||||
|
value: c,
|
||||||
|
name: this.categoriesNames[c] ?? "UNKNOWN"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredPhotos() {
|
||||||
|
if (this.selectedCategory === this.ALL_CATEGORY || this.selectedCategory === null) {
|
||||||
|
return this.images;
|
||||||
|
} else {
|
||||||
|
return this.images.filter((photo) => photo.category === this.selectedCategory);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
filteredPhotos: function () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshGallery()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refreshGallery() {
|
||||||
|
// Update LightGallery elements (click ability etc)
|
||||||
|
this.lg.refresh()
|
||||||
|
|
||||||
|
// Filter Shuffle & prevent from breaking the layout
|
||||||
|
this.shuffle.resetItems()
|
||||||
|
|
||||||
|
// Jerky animation fix
|
||||||
|
setTimeout(() => {
|
||||||
|
this.shuffle.filter(this.selectedCategory);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
galleryInit(element) {
|
||||||
|
this.lg = lightGallery(element, {
|
||||||
|
plugins: [lgThumbnail, lgZoom, lgFullScreen],
|
||||||
|
mode: "lg-fade", // animation mode for fullscreen images
|
||||||
|
selector: '.gallery-item',
|
||||||
|
swipeToClose: false,
|
||||||
|
speed: 400,
|
||||||
|
alignThumbnails: "left",
|
||||||
|
download: false,
|
||||||
|
fullScreen: true,
|
||||||
|
mobileSettings: {
|
||||||
|
controls: true,
|
||||||
|
showCloseIcon: true,
|
||||||
|
thumbnail: false,
|
||||||
|
closeOnTap: false,
|
||||||
|
fullScreen: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
shuffleInit(element) {
|
||||||
|
this.shuffle = new Shuffle(element, {
|
||||||
|
group: this.initialCategory,
|
||||||
|
itemSelector: '.gallery-item',
|
||||||
|
delimiter: ',',
|
||||||
|
speed: 500,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
let $gallery = document.querySelector('#gallery')
|
||||||
|
|
||||||
|
this.galleryInit($gallery)
|
||||||
|
|
||||||
|
ImagesLoaded('#gallery', {background: true}, () => {
|
||||||
|
console.log("ImagesLoaded")
|
||||||
|
|
||||||
|
this.shuffleInit($gallery)
|
||||||
|
this.galleryLoading = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='css'>
|
||||||
|
@import 'lightgallery/css/lightgallery.css';
|
||||||
|
@import 'lightgallery/css/lg-thumbnail.css';
|
||||||
|
@import 'lightgallery/css/lg-zoom.css';
|
||||||
|
@import 'lightgallery/css/lg-fullscreen.css';
|
||||||
|
|
||||||
|
</style>
|
||||||
23
components/gallery/GalleryItem.vue
Normal file
23
components/gallery/GalleryItem.vue
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import GalleryItemMixin from "~/components/gallery/GalleryItemMixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [GalleryItemMixin],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a class="gallery-item w-1/2 p-0.5 sm:w-1/2 md:w-1/3 xl:w-1/4 sm:p-1"
|
||||||
|
:class="`filter-${category}`"
|
||||||
|
:data-groups="category"
|
||||||
|
:data-src="full_img_url"
|
||||||
|
>
|
||||||
|
<img class="gallery-item-thumb cursor-pointer w-full"
|
||||||
|
:width="thumbSize.w" :height="thumbSize.h"
|
||||||
|
:src="thumbnail_url"/>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gallery-item {}
|
||||||
|
</style>
|
||||||
20
components/gallery/GalleryItemMixin.js
Normal file
20
components/gallery/GalleryItemMixin.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
category: {type: String, required: true},
|
||||||
|
image: {type: String, required: true},
|
||||||
|
thumbSize: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default: () => ({w: 0, h: 0})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
full_img_url: function () {
|
||||||
|
return `/gallery/${this.category}/${this.image}`
|
||||||
|
},
|
||||||
|
thumbnail_url: function () {
|
||||||
|
return `/gallery/${this.category}/thumbs/${this.image}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
58
components/gallery/HorizontalGallery.vue
Normal file
58
components/gallery/HorizontalGallery.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div ref="slider" class="keen-slider my-24">
|
||||||
|
<HorizontalGalleryItem
|
||||||
|
v-for="img in images"
|
||||||
|
:key="img.id"
|
||||||
|
:image="img.image"
|
||||||
|
:category="img.category"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import "keen-slider/keen-slider.min.css";
|
||||||
|
import KeenSlider from "keen-slider";
|
||||||
|
import GalleryItemMixin from "~/components/gallery/GalleryItemMixin";
|
||||||
|
import HorizontalGalleryItem from "~/components/gallery/HorizontalGalleryItem.vue";
|
||||||
|
|
||||||
|
const animation = {duration: 40000, easing: (t) => t}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "HorizontalGallery",
|
||||||
|
components: {HorizontalGalleryItem},
|
||||||
|
props: {
|
||||||
|
images: {type: Array, default: []},
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [GalleryItemMixin],
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.slider = new KeenSlider(this.$refs.slider, {
|
||||||
|
loop: true,
|
||||||
|
renderMode: "performance",
|
||||||
|
drag: false,
|
||||||
|
slides: {perView: "auto"},
|
||||||
|
|
||||||
|
created(s) {
|
||||||
|
s.moveToIdx(5, true, animation)
|
||||||
|
},
|
||||||
|
updated(s) {
|
||||||
|
s.moveToIdx(s.track.details.abs + 5, true, animation)
|
||||||
|
},
|
||||||
|
animationEnded(s) {
|
||||||
|
s.moveToIdx(s.track.details.abs + 5, true, animation)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.slider) this.slider.destroy();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
</style>
|
||||||
17
components/gallery/HorizontalGalleryItem.vue
Normal file
17
components/gallery/HorizontalGalleryItem.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div class="keen-slider__slide">
|
||||||
|
<img class="" :src="thumbnail_url"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GalleryItemMixin from "~/components/gallery/GalleryItemMixin";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "HorizontalGalleryItem",
|
||||||
|
mixins: [GalleryItemMixin],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
</style>
|
||||||
96
components/shared/AppBanner.vue
Normal file
96
components/shared/AppBanner.vue
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script>
|
||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="
|
||||||
|
background
|
||||||
|
static
|
||||||
|
grayscale
|
||||||
|
min-h-screen
|
||||||
|
max-h-screen
|
||||||
|
|
||||||
|
md:pl-12
|
||||||
|
xl:pl-16
|
||||||
|
|
||||||
|
md:pt-20
|
||||||
|
lg:pt-28
|
||||||
|
pt-16
|
||||||
|
">
|
||||||
|
|
||||||
|
<div class="xl:w-3/4 mr-auto mx-4 my-20 text-left animate-slide-up">
|
||||||
|
<h1
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
text-5xl
|
||||||
|
sm:text-7xl
|
||||||
|
md:text-7xl
|
||||||
|
lg:text-8xl
|
||||||
|
|
||||||
|
text-left
|
||||||
|
|
||||||
|
mb-2
|
||||||
|
sm:mb-4
|
||||||
|
|
||||||
|
text-gray-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Привет, это Саша.
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="
|
||||||
|
font-general-medium
|
||||||
|
mb-8
|
||||||
|
|
||||||
|
text-2xl
|
||||||
|
sm:text-3xl
|
||||||
|
md:text-3xl
|
||||||
|
xl:text-3xl
|
||||||
|
|
||||||
|
leading-none
|
||||||
|
text-gray-300
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Интерьерный и репортажный фотограф
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<NuxtLink :to="{path: '/', hash: '#contact'}">
|
||||||
|
<RoundButton class="text-xl px-6 sm:px-8">
|
||||||
|
Написать мне
|
||||||
|
</RoundButton>
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Banner right illustration -->
|
||||||
|
<!-- <div class="w-full md:w-3/5 text-right float-right">-->
|
||||||
|
<!-- <img src="/photographer.svg" alt="Photographer illustration"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.background {
|
||||||
|
/* TODO: grayscale the file itself to reduce file size */
|
||||||
|
/*background-image: url("/background.jpg");*/
|
||||||
|
background-image: url("/background-mirrored.jpg");
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
components/shared/AppHeader.vue
Normal file
125
components/shared/AppHeader.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script>
|
||||||
|
import {mapState, mapMutations} from "vuex";
|
||||||
|
import AppNavigation from "./AppNavigation.vue";
|
||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["showMobileMenu"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
AppNavigation,
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
modal: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["socialProfiles"]),
|
||||||
|
|
||||||
|
isMobileMenuOpened() {
|
||||||
|
return this.$store.state.isMobileMenuOpened
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// Watch for route change to hide mobile menu
|
||||||
|
isMobileMenuOpened() {
|
||||||
|
feather.replace()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
themeSwitcher() {
|
||||||
|
this.$colorMode.preference =
|
||||||
|
this.$colorMode.value === "light" ? "dark" : "light";
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.$store.commit('toggleMobileMenu')
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleModal() {
|
||||||
|
if (this.modal) {
|
||||||
|
// Stop screen scrolling
|
||||||
|
document
|
||||||
|
.getElementsByTagName("html")[0]
|
||||||
|
.classList.remove("overflow-y-hidden");
|
||||||
|
this.modal = false;
|
||||||
|
} else {
|
||||||
|
document
|
||||||
|
.getElementsByTagName("html")[0]
|
||||||
|
.classList.add("overflow-y-hidden");
|
||||||
|
this.modal = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav id="nav" class="top-0 z-50 absolute w-full">
|
||||||
|
<div class="sm:w-full sm:px-14 px-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
z-10
|
||||||
|
block
|
||||||
|
sm:flex sm:justify-between sm:items-center
|
||||||
|
py-6
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- Header menu links and small screen hamburger menu -->
|
||||||
|
<div class="flex justify-between items-center px-6 sm:px-0">
|
||||||
|
<!-- Header logos -->
|
||||||
|
<div>
|
||||||
|
<NuxtLink to="/">
|
||||||
|
<img
|
||||||
|
src="/logo-white.png"
|
||||||
|
alt="Logo"
|
||||||
|
class="sm:w-32 w-24"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Small screen hamburger menu -->
|
||||||
|
<div class="sm:hidden">
|
||||||
|
<button
|
||||||
|
@click="toggleMobileMenu()"
|
||||||
|
type="button"
|
||||||
|
class="focus:outline-none"
|
||||||
|
aria-label="Hamburger Menu"
|
||||||
|
>
|
||||||
|
<div class="h-7 w-7 mt-1 fill-current text-white">
|
||||||
|
<div v-show="!isMobileMenuOpened"><i data-feather="menu"/></div>
|
||||||
|
<div v-show="isMobileMenuOpened"><i data-feather="x"/></div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header links -->
|
||||||
|
<AppNavigation
|
||||||
|
:toggleModal="toggleModal"
|
||||||
|
:modal="modal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<SocialButton v-for="social in socialProfiles"
|
||||||
|
:key="social.id"
|
||||||
|
:feather-icon="social.icon"
|
||||||
|
:url="social.url"
|
||||||
|
class="hidden md:block"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
101
components/shared/AppNavigation.vue
Normal file
101
components/shared/AppNavigation.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<script>
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["toggleModal", "modal"],
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
linkStyle: "block text-left text-xl " +
|
||||||
|
"text-white " +
|
||||||
|
"hover:text-accent transition-colors duration-400 " +
|
||||||
|
"sm:mx-3 md:mx-4 sm:pt-2 mb-2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["socialProfiles"]),
|
||||||
|
|
||||||
|
isMobileMenuOpened() {
|
||||||
|
return this.$store.state.isMobileMenuOpened
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onMobileMenuLinkClick() {
|
||||||
|
this.$store.commit('toggleMobileMenu', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- App header navigation links -->
|
||||||
|
<div
|
||||||
|
:class="isMobileMenuOpened ? 'block' : 'hidden'"
|
||||||
|
class="
|
||||||
|
font-general-regular
|
||||||
|
|
||||||
|
m-0
|
||||||
|
mt-5
|
||||||
|
p-5
|
||||||
|
py-12
|
||||||
|
|
||||||
|
backdrop-blur-lg
|
||||||
|
justify-center
|
||||||
|
items-center
|
||||||
|
animate-slide-down-fast
|
||||||
|
|
||||||
|
sm:backdrop-filter-none
|
||||||
|
sm:animate-none
|
||||||
|
sm:mt-0
|
||||||
|
sm:flex
|
||||||
|
sm:p-0
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
:to="{path: '/', hash: '#portfolio'}"
|
||||||
|
:class="linkStyle"
|
||||||
|
aria-label="Portfolio"
|
||||||
|
@click.native="onMobileMenuLinkClick"
|
||||||
|
>
|
||||||
|
Портфолио
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
:to="{path: '/', hash: '#about'}"
|
||||||
|
:class="linkStyle"
|
||||||
|
class="
|
||||||
|
border-t-2
|
||||||
|
pt-3
|
||||||
|
sm:pt-2 sm:border-t-0
|
||||||
|
border-primary-light
|
||||||
|
|
||||||
|
"
|
||||||
|
aria-label="About Me"
|
||||||
|
@click.native="onMobileMenuLinkClick"
|
||||||
|
>
|
||||||
|
Обо мне
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink
|
||||||
|
:to="{path: '/', hash: '#contact'}"
|
||||||
|
:class="linkStyle"
|
||||||
|
class="
|
||||||
|
border-t-2
|
||||||
|
pt-3
|
||||||
|
sm:pt-2 sm:border-t-0
|
||||||
|
border-primary-light
|
||||||
|
|
||||||
|
"
|
||||||
|
aria-label="Contact"
|
||||||
|
@click.native="onMobileMenuLinkClick"
|
||||||
|
>
|
||||||
|
Контакты
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
71
layouts/default.vue
Normal file
71
layouts/default.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<div class="bg-background-dark min-h-screen overscroll-none">
|
||||||
|
<!-- App header -->
|
||||||
|
<AppHeader/>
|
||||||
|
|
||||||
|
<!-- Render contents with transition -->
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<Nuxt/>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<!-- Go back to top when scrolled down -->
|
||||||
|
<SideBar/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import feather from "feather-icons";
|
||||||
|
import smoothscroll from 'smoothscroll-polyfill';
|
||||||
|
|
||||||
|
if (process.browser) {
|
||||||
|
smoothscroll.polyfill();
|
||||||
|
}
|
||||||
|
|
||||||
|
import AppBanner from "@/components/shared/AppBanner.vue";
|
||||||
|
import AppHeader from "../components/shared/AppHeader.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {AppBanner, AppHeader},
|
||||||
|
data: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
// feather.replace();
|
||||||
|
try {
|
||||||
|
await this.$recaptcha.init()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes going {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes coming {
|
||||||
|
from {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateX(0px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
66
nuxt.config.js
Normal file
66
nuxt.config.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
export default {
|
||||||
|
server: {
|
||||||
|
host: '0' // default: localhost
|
||||||
|
},
|
||||||
|
|
||||||
|
// Target: https://go.nuxtjs.dev/config-target
|
||||||
|
target: "static",
|
||||||
|
colorMode: {
|
||||||
|
classSuffix: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global page headers: https://go.nuxtjs.dev/config-head
|
||||||
|
head: {
|
||||||
|
title: "Alex Sharoff",
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: "en"
|
||||||
|
},
|
||||||
|
meta: [
|
||||||
|
{ charset: "utf-8" },
|
||||||
|
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
||||||
|
{ hid: "description", name: "description", content: "" },
|
||||||
|
{ name: "format-detection", content: "telephone=no" },
|
||||||
|
],
|
||||||
|
link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.png" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||||
|
css: [
|
||||||
|
"~/assets/app.css",
|
||||||
|
"~/assets/fonts/FuturaPT/futura-pt.css"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||||
|
plugins: [
|
||||||
|
// { src: '~/plugins/vue-isotope', mode: 'client' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Auto import components: https://go.nuxtjs.dev/config-components
|
||||||
|
components: true,
|
||||||
|
|
||||||
|
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
|
||||||
|
buildModules: [
|
||||||
|
"@nuxtjs/tailwindcss",
|
||||||
|
"@nuxtjs/color-mode",
|
||||||
|
],
|
||||||
|
|
||||||
|
// Modules: https://go.nuxtjs.dev/config-modules
|
||||||
|
modules: [
|
||||||
|
'@nuxtjs/recaptcha',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||||
|
build: {
|
||||||
|
// extend (config, ctx) {
|
||||||
|
// if (ctx.isDev) {
|
||||||
|
// config.devtool = ctx.isClient ? 'source-map' : 'inline-source-map'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
recaptcha: {
|
||||||
|
hideBadge: true,
|
||||||
|
siteKey: '6LeGbUElAAAAACaZos82QiUanP78WgZTrc4KCCRj',
|
||||||
|
version: 2
|
||||||
|
}
|
||||||
|
};
|
||||||
31
package.json
Normal file
31
package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "nuxtjs-tailwindcss-portfolio",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt start",
|
||||||
|
"generate": "nuxt generate"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxtjs/recaptcha": "^1.1.2",
|
||||||
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
|
"@tailwindcss/forms": "^0.3.4",
|
||||||
|
"core-js": "^3.15.1",
|
||||||
|
"feather-icons": "^4.28.0",
|
||||||
|
"imagesloaded": "^5.0.0",
|
||||||
|
"keen-slider": "^6.8.5",
|
||||||
|
"lightgallery": "^2.7.1",
|
||||||
|
"nuxt": "^2.15.7",
|
||||||
|
"shufflejs": "^6.1.0",
|
||||||
|
"smoothscroll-polyfill": "^0.4.4",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
|
"vanilla-lazyload": "^17.8.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxtjs/color-mode": "^2.1.1",
|
||||||
|
"@nuxtjs/tailwindcss": "^4.2.0",
|
||||||
|
"postcss": "^8.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
pages/index.vue
Normal file
55
pages/index.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script>
|
||||||
|
import {mapState, mapGetters} from "vuex";
|
||||||
|
import feather from "feather-icons";
|
||||||
|
|
||||||
|
import AppBanner from "@/components/shared/AppBanner.vue";
|
||||||
|
import Gallery from "@/components/gallery/Gallery.vue";
|
||||||
|
import AboutMe from "@/components/about/AboutMe.vue";
|
||||||
|
import AboutClients from "@/components/about/AboutClients.vue";
|
||||||
|
import ContactForm from "@/components/contact/ContactForm.vue";
|
||||||
|
import ContactDetails from "@/components/contact/ContactDetails.vue";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
scrollToTop: true,
|
||||||
|
components: {AppBanner, Gallery, AboutMe, AboutClients, ContactForm, ContactDetails},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(["photos", "initialCategory", "contacts"]),
|
||||||
|
...mapGetters(["showcasePhotos"])
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
feather.replace();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<AppBanner/>
|
||||||
|
|
||||||
|
<div class="container mx-auto mt-20">
|
||||||
|
<Gallery id="portfolio"
|
||||||
|
:images="photos"
|
||||||
|
:initial-category="initialCategory"
|
||||||
|
:filter-enabled="true"/>
|
||||||
|
|
||||||
|
<AboutMe id="about" class=""/>
|
||||||
|
<AboutClients class=""/>
|
||||||
|
|
||||||
|
<div id="contact" class="flex flex-col-reverse md:flex-row md:py-10 md:mt-20">
|
||||||
|
<ContactForm class="w-full md:w-3/5"/>
|
||||||
|
<ContactDetails class="w-full md:w-2/5" :contacts="contacts"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user