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