This commit is contained in:
choelzl 2021-07-16 09:12:30 +02:00
commit 3c0bd54d54
Signed by: sora
GPG Key ID: A362EA0491E2EEA0
55 changed files with 25927 additions and 0 deletions

12
.drone.yml Normal file
View File

@ -0,0 +1,12 @@
pipeline:
deploy-production:
image: docker/compose
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker-compose -p otm down
- docker-compose build --no-cache otm
- docker-compose -p otm up -d
when:
event: push
branch: [master]

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
package-lock.json
node_modules/*
auth/*
db/*

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM node
RUN apt-get update && apt-get install -y pandoc pandoc-data texlive-latex-recommended pandoc-citeproc texlive-latex-extra python python-pip
RUN pip install pandocfilters
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "node", "server.js" ]

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# volp
Visual Online Language Processor

36
auth_db.js Normal file
View File

@ -0,0 +1,36 @@
'use strict'
const fp = require('fastify-plugin')
const levelup = require('levelup')
const leveldown = require('leveldown')
const encode = require('encoding-down')
// mostly from level-packager
const levelMore = (location) => {
;['destroy', 'repair'].forEach(function (m) {
if (typeof leveldown[m] === 'function') {
levelMore[m] = () => leveldown[m].apply(leveldown, arguments)
}
})
return levelup(encode(leveldown(location), { }), { })
}
levelMore.errors = levelup.errors
function levelPlugin (fastify, opt, next) {
fastify
.decorate('lauth', levelMore('auth'))
.addHook('onClose', close)
next()
}
function close (fastify, done) {
fastify.level.close(done)
}
module.exports = fp(levelPlugin, {
fastify: '>=1.0.0',
name: 'level_auth'
});

21
compile_pandoc.js Normal file
View File

@ -0,0 +1,21 @@
const pandoc = require('node-pandoc');
const path = require('path');
module.exports = (KEY)=>{
this.key = KEY;
this.compile_cv = (src,style)=>{
return new Promise((resolve,reject)=>{
style = "default-otm";
let args = `-s --section-divs --template template/${style}.html -f markdown+raw_html+header_attributes+definition_lists+yaml_metadata_block -t html5`;
/* --variable=date:'$(DATE)' \ */
pandoc(src, args, (err, res)=>{
if (err) reject(err);
return resolve(res);
});
})
}
return this;
}

70
compile_sass.js Normal file
View File

@ -0,0 +1,70 @@
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const sass = require('node-sass');
const nodeEnv = process.env.NODE_ENV;
module.exports = {
compileSass,
compileSassProduction,
compileSassMain
};
function compileSass(sassFile) {
const sassOptions = {
file: sassFile
};
if (nodeEnv !== 'production') {
sassOptions.sourceMapEmbed = true;
}
else {
sassOptions.outputStyle = 'compressed';
}
return new Promise((resolve, reject) => {
sass.render(sassOptions, (error, result) => {
if (error) {
return reject(error);
}
resolve(result.css.toString());
});
}).catch(console.error);
}
function compileSassProduction(sassFile) {
const fullSassPath = path.join(__dirname, 'public/scss/', sassFile);
const cssFile = sassFile.replace('.scss', '.css');
const cssPath = path.join(__dirname, 'public/css/');
const fullCssPath = path.join(cssPath, cssFile);
return compileSass(fullSassPath).then(css => {
return new Promise((resolve, reject) => {
mkdirp(cssPath, error => {
if (error) {
return reject(error);
}
resolve();
});
}).then(() => {
return new Promise((resolve, reject) => {
fs.writeFile(fullCssPath, css, error => {
if (error) {
return reject(error);
}
resolve(cssFile);
});
});
}).catch(console.error);
});
}
function compileSassMain() {
return compileSassProduction('index.scss').then(() => {
console.log('Created index.css');
}).catch(console.error);
}

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: "3.7"
networks:
external:
external: true
services:
otm:
build: ./
container_name: otm
networks:
- external
labels:
- "traefik.enable=true"
- "traefik.http.routers.otm.entrypoints=web-secure"
- "traefik.http.routers.otm.rule=Host(`otm.helcel.net`)"
- "traefik.http.routers.otm.tls=true"
- "traefik.http.services.otm.loadbalancer.server.port=8080"
- "traefik.docker.network=external"
restart: always

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "volp",
"version": "1.0.0",
"description": "Visual Online Language Processor",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "git@git.helcel.net:sora/volp.git"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"ejs": "^2.7.4",
"fastify": "^2.10.0",
"fastify-cookie": "^3.2.0",
"fastify-jwt": "^1.2.0",
"fastify-leveldb": "^2.1.0",
"fastify-static": "^2.5.0",
"fastify-websocket": "^1.0.0",
"node-pandoc": "^0.3.0",
"node-sass": "^6.0.0",
"point-of-view": "^3.7.0",
"ws": "^7.2.0"
}
}

4
public/css/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4009
public/css/index.css Normal file

File diff suppressed because one or more lines are too long

BIN
public/fonts/fa-brands.ttf Normal file

Binary file not shown.

BIN
public/fonts/fa-regular.ttf Normal file

Binary file not shown.

BIN
public/fonts/fa-solid.ttf Normal file

Binary file not shown.

BIN
public/img/customizable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/img/helcel.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/img/helcel.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

15073
public/img/helcel.svg Executable file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/img/lightweight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
public/img/opensource.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/js/.DS_Store vendored Normal file

Binary file not shown.

7
public/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

242
public/js/main.js Normal file
View File

@ -0,0 +1,242 @@
const gen_id = (length)=>{
var result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const len = characters.length;
for (let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * len));
}
return result;
}
const query_nominatim = (q,f)=>{
const ENDPOINT = 'https://nominatim.openstreetmap.org/';
const FORMAT = 'jsonv2';
return axios.get(ENDPOINT, {
params: {
format: FORMAT,
q:q,
},
}).then(res=>res.data).then(res=>res.filter(f));
}
const query_flight = (q)=>{
const ENDPOINT = '/api/flight/'+q;
const FORMAT = '-';
return axios.get(ENDPOINT).then(res=>res.data);
}
const is_restauration_type = (t)=>{
const arr = ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"];
return arr.indexOf(t)!=-1;
}
const is_attraction_type = (item)=>{
const arr = ["tourism"];
return arr.indexOf(item.category)!=-1;
}
const icon_type = (item)=>{
let t = item.type
const arr = ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"];
if(arr.indexOf(t)!=-1){
return 'utensils';
}else if(t=='hotel'){
return 'bed';
}else if(t=='museum'){
return 'landmark';
}else if(t=='peak'){
return 'mountain';
}else if(t=='parking'){
return 'parking';
}else if(t=='water' || t=='river'){
return 'water';
}else if(t=='community_centre'){
return 'building';
}else if(t=='attraction'){
return 'landmark';
}else if(t=='information'){
return 'landmark';
}else if(t=='bridge'){
return 'archway';
}else if(t=='a'){
return '';
}else if(t=='a'){
return '';
}else{
console.log(item.category, item.type);
return 'question';
}
}
Vue.component('l-map', window.Vue2Leaflet.LMap);
Vue.component('l-tile-layer', window.Vue2Leaflet.LTileLayer);
Vue.component('l-marker', window.Vue2Leaflet.LMarker);
Vue.component('l-icon', window.Vue2Leaflet.LIcon);
Vue.component('l-popup', window.Vue2Leaflet.LPopup);
Vue.component('l-tooltip', window.Vue2Leaflet.LTooltip);
Vue.component('l-control-scale', window.Vue2Leaflet.LControlScale);
Vue.component('l-draggable', window.vuedraggable);
Vue.use(window.VueTextareaAutosize.install)
Vue.component('multiselect', window.VueMultiselect.default)
const app = new Vue({
el: '#app',
data: {
journey_id : window.location.pathname.slice(1) || gen_id(16),
journey_step: window.location.hash.slice(1)==""?-1:parseInt(window.location.hash.slice(1)),
journey_step_data: {day:-1, section:-1},
journey_data : {
name: "New Journey",
main:[],
step_title:[],
},
query:{hotel:[],flight:[],nominatim:[]},
querying:{hotel:false,flight:false,place:false,food:false},
impexp:"",
activities_tmp:[],
},
methods: {
start_journey: function(event){
window.location.href = '/'+this.journey_id;
},
add_section: function(event){
if(this.journey_data.main===undefined) this.journey_data.main=[];
this.journey_data.main.push({map:{zoom:2}, hotel:{latlon:[0,0]},places:{restaurants:[],places:[]}});
},
next_step: function(){
this.journey_step+=1;
this.journey_step_data = this.step_convert(this.journey_step);
this.activities_tmp = this.journey_data.main[this.journey_step_data.section].places.activities.filter(e=>e.step==this.journey_step)
window.location.hash = '#' + this.journey_step;
},
first_step: function(){
this.journey_step=-1;
this.journey_step_data = {day:-1, section:-1};
this.activities_tmp = [];
window.location.hash = '';
},
step_convert: function(step){
let steps_left = step+1;
for(let e in this.journey_data.main){
if(this.journey_data.main[e].dateRange && (this.journey_data.main[e].dateRange[0]-(new Date(0))) != 0){
let cd = (this.journey_data.main[e].dateRange[1]-this.journey_data.main[e].dateRange[0])/(1000*60*60*24);
if(cd>=steps_left){
return {day: steps_left, section:e,start: (steps_left==1), end: (steps_left==cd)};
}else{
steps_left -= cd;
}
}else{
steps_left -= 1;
}
if(steps_left==0){
return {day:0,section:e, start:true,end:true};
}
}
return {day:-1, section:-1};
},
rm_section: function(idx){
this.journey_data.main.splice(idx,1);
},
search_nominatim: function(txt,f){
if(txt==""){
this.query.nominatim=[];
return Promise.resolve([]);
}
return query_nominatim(txt,f)
.then((results) =>{
results.forEach(r=>r.latlon=[parseFloat(r.lat),parseFloat(r.lon)])
this.query.nominatim=results;
});
},
search_flight: function(txt){
if(txt=="") return;
this.querying.flight=true;
query_flight(txt.replace(" ", ""))
.then((results) =>{
if(results.results==""){
this.query.flight=[];this.querying.flight=false;return;
}
this.query.flight=results.results;
this.querying.flight=false;
});
},
generate_icon: function(item){
return L.AwesomeMarkers.icon(
{icon: icon_type(item) || 'star', prefix: 'fa',markerColor: item.color || 'blue'}
).createIcon().outerHTML
},
save_data: function(){
this.impexp = window.btoa(JSON.stringify(this.journey_data));
axios.post('/api/'+this.journey_id, this.journey_data).then(response => {
console.log("Saved...")
}).catch(error => {
console.warn('Error! Could not reach the API.')
})
},
import_data:function(){
this.journey_data = JSON.parse(window.atob(this.impexp));
},
export_data:function(){
this.impexp = window.btoa(JSON.stringify(this.journey_data));
},
filter_selected:function(list,step){
return list.filter(e=>(step?(e.step==this.journey_step):(e.step>=0)))
},
filter_unselected:function(list){
return list.filter(e=>(e.step==undefined || e.step<0))
},
remove_item:function(list,idx){
list[idx].step=-1;
list.splice(idx, 1);
},
log:function(e){console.log(e)},
},
created: function () {
axios.get('/api/'+this.journey_id).then(response =>{
app.journey_data = response.data;
for(let e of app.journey_data.main){
if(e.dateRange && e.dateRange.length===2){
e.dateRange[0]= new Date(e.dateRange[0]);
e.dateRange[1]= new Date(e.dateRange[1]);
}
}
this.journey_step_data = this.step_convert(this.journey_step);
this.activities_tmp = this.journey_data.main[this.journey_step_data.section].places.activities.filter(e=>e.step==this.journey_step);
});
this.debounceSave = _.debounce(this.save_data, 500)
this.debounceSearch = {"hotel":_.debounce((q)=>{this.querying.hotel=true;this.search_nominatim(q,(r)=>(r.type=="hotel")).then((r)=>{this.querying.hotel=false});}, 500),
"restaurants":_.debounce((q)=>{this.querying.food=true;this.search_nominatim(q,(r)=>(is_restauration_type(r.type))).then((r)=>{this.querying.food=false});}, 500),
"places":_.debounce((q)=>{this.querying.place=true;this.search_nominatim(q,(r)=>(true)).then((r)=>{this.querying.place=false});}, 500),
"other":_.debounce((q)=>{this.querying.any=true;this.search_nominatim(q,(r)=>(true)).then((r)=>{this.querying.any=false});}, 500),
"flight":_.debounce((q)=>this.search_flight(q), 500)
}
},
watch: {
journey_data: {
handler:function (ndata, odata){
this.debounceSave()
if(ndata.main.length == odata.main.length){
for(let d in ndata.main){
if(ndata.main[d].hotel != odata.main[d].hotel){
if(ndata.main[d].hotel.latlon){
this.journey_data.main[d].map.center=ndata.main[d].hotel.latlon;
}
}
}
}
},
deep: true,
},
activities_tmp:{
handler:function (nd,od){
nd.forEach(e=>e.step=this.journey_step);
},
deep: false,
},
},
})

BIN
public/scss/.DS_Store vendored Normal file

Binary file not shown.

261
public/scss/_settings.scss Normal file
View File

@ -0,0 +1,261 @@
/**
* SETTINGS
*/
$enable-grid-classes: true !default; // generate grid classes
$enable-reset-defaults: true !default; // reset default browser values
// colors
$c-white: #fff !default;
$c-black: #000 !default;
$c-success: #4caf50 !default;
$c-info: #5bc0de !default;
$c-warning: #f0ad4e !default;
$c-error: #e74c3c !default;
$c-gray: #969da6 !default;
$c-gray-light: #eceff1 !default;
$c-primary: #03a9f4 !default;
$c-secondary: #e91e63 !default;
$c-dark: #18232f !default;
$c-light: $c-gray-light !default;
$c-text: #272727 !default;
$c-link: $c-primary !default;
$c-link-hover: rgba($c-primary, .8) !default;
$bgc-body: $c-white !default;
$c-map: (
'primary': $c-primary,
'dark': $c-dark,
'secondary': $c-secondary,
'white': $c-white,
'success': $c-success,
'info': $c-info,
'warning': $c-warning,
'error': $c-error,
'gray': $c-gray,
'gray-light': $c-gray-light
) !default;
// Social colors
$c-fb: rgb(59, 89, 182) !default;
$c-tw: rgb(64, 153, 255) !default;
$c-googleplus: rgb(215, 61, 50) !default;
$c-pinterest: rgb(203, 32, 39) !default;
$c-skype: rgb(18, 165, 244) !default;
$c-spotify: rgb(129, 183, 26) !default;
$c-instagram: #4e433c !default;
$c-tumblr: #2b4964 !default;
$c-vimeo: #86b32d !default;
$c-soundcloud: #f76600 !default;
$c-youtube: #ff3333 !default;
$c-linkedin: #4875b4 !default;
$c-flickr: #fe0883 !default;
$c-foursquare: #8fd400 !default;
// Shadows
$bxsh-base: 0 0 5px rgba(0, 0, 0, .2) !default;
$bxsh-medium: $bxsh-base !default;
// Typography
$fw-light: 300 !default;
$fw-normal: 400 !default;
$fw-bold: 600 !default;
$fw-headings: $fw-light !default;
$ff-base: 'IBM Plex Sans', sans-serif !default;
$fw-base: 400 !default;
$fz-base: 16px !default;
$lh-base: 1.5em !default;
$fz-huge: 36px !default;
$fz-big: 24px !default;
$fz-medium: $fz-base !default;
$fz-small: 12px !default;
$lh-huge: 1.3em !default;
$lh-big: 1.3em !default;
$lh-medium: $lh-base !default;
$lh-small: 1.3em !default;
// Body
$c-body: $c-text !default;
$ff-body: $ff-base !default;
$fz-body: $fz-base !default;
$fw-body: $fw-base !default;
$lh-body: $lh-base !default;
// Border
$bdc-base: #d5d9db !default;
$bd-medium: 1px solid $bdc-base !default;
$bdr-base: 3px !default;
$bdr-medium: $bdr-base !default;
$bdr-rounded-corners: 5px !default;
// Defaults (HTML elements)
$fw-strong: $fw-bold !default;
$mb-p: 1.5em !default;
$mb-ul: 1em !default;
$mb-li: .5em !default;
// Margin
$m-xsmall: 4px !default;
$m-small: 8px !default;
$m-medium: 16px !default;
$m-big: 36px !default;
$m-huge: 48px !default;
// Padding
$p-small: 4px !default;
$p-medium: 8px !default;
$p-big: 16px !default;
$p-huge: 36px !default;
// Layout
$bgc-container: transparent !default;
$mwi-container: 1380px !default;
$mwi-container-medium: 944px !default;
$mwi-container-small: 400px !default;
$w-gutter: 24px !default;
// Grid
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
) !default;
$grid-columns: 12 !default;
$grid-gutter-width: $w-gutter !default;
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px
) !default;
/**
* COMPONENTS
*/
// Badges
$c-badge: $c-text !default;
$c-badge-colored: $c-white !default;
$bg-badge: $c-light !default;
$bdr-badge: $bdr-medium !default;
$lh-badge: 1.2em !default;
$p-badge: $p-medium $p-big !default;
$fz-badge-big: 1.3em !default;
$fz-badge-small: .7em !default;
$badges-color-map: (
primary: $c-primary,
secondary: $c-secondary,
dark: $c-dark,
light: $c-gray,
success: $c-success,
error: $c-error,
warning: $c-warning
) !default;
// Buttons
$bg-button: $c-primary !default;
$c-button: $c-white !default;
$c-button-transparent: $c-primary !default;
$c-button-outlined: $c-primary !default;
$bdc-button: $bg-button !default;
$bd-button: 1px solid $bdc-button !default;
$bdr-button: 200px !default;
$ff-button: $ff-body !default;
$fz-button: $fz-medium !default;
$fw-button: $fw-bold !default;
$lh-button: $lh-body !default;
$m-button: 0 $m-small $m-medium $m-small !default;
$p-button: $p-medium $p-big !default;
$tt-button: uppercase !default;
$fz-button-big: $fz-big !default;
$p-button-big: $p-big $p-huge !default;
$fz-button-small: $fz-small !default;
$p-button-small: $p-small $p-big !default;
// Forms
$bgc-form: $c-light !default;
$c-form: $c-text !default;
$bdr-form: $bdr-medium !default;
$bd-form: $bd-medium !default;
$fz-form: $fz-body !default;
$bgc-form-focus: lighten($bgc-form, 5%) !default;
$trs-form: background-color .2s ease-in-out !default;
$c-form-placeholder: $c-gray !default;
$mih-form-textarea: 120px !default;
$p-form: $p-medium $p-big !default;
$size-form-input-icon: 16px !default;
$c-form-input-icon: $c-gray !default;
$bd-form-error: 1px solid $c-error !default;
$bgc-form-error: $bgc-form !default;
// Loading bar
$h-loading-bar: 6px !default;
$w-loading-bar: 300px !default;
$z-loading-bar: 1000 !default;
// Loading spinner
$bg-loading-spinner: $c-secondary !default;
$size-loading-spinner-inner: 9px !default;
$size-loading-spinner: 30px !default;
$z-loading-spinner: 1000 !default;
// Notification
$bdr-notification: $bdr-medium !default;
$c-notification: $c-white !default;
$mb-notification: $m-big !default;
$p-notification: $p-big !default;
// Paginator
$mr-paginator-item: $m-xsmall !default;
$bdr-paginator-item: $bdr-medium !default;
$bg-paginator-item: $c-light !default;
$c-paginator-item: $c-text !default;
$bg-paginator-item-active: $c-primary !default;
$c-paginator-item-active: $c-white !default;
$p-paginator-item: $p-medium $p-big !default;
// Table
$bgc-table-header: $bgc-body !default;
$bgc-table: $c-light !default;
$bd-table: $bd-medium !default;
$c-table-heading: $c-gray !default;
$c-table-text: $c-text !default;
$fw-table-heading: normal !default;
$mwi-table: 100% !default;
$p-table-cell: $p-medium !default;
$p-table-heading: $p-medium !default;
$w-table: 100% !default;
$fz-table-heading: $fz-small !default;
// Tabs
$bdb-tabs-item-selected: 3px solid $c-primary !default;
$bdb-tabs-item: 3px solid transparent !default;
$bdb-tabs: $bd-medium !default;
$c-tabs-item-hover: $c-primary !default;
$c-tabs-item-selected: $c-primary !default;
$c-tabs-item: $c-gray !default;
$m-tabs-item: 0 $m-medium 0 0 !default;
$miw-tabs-item: 70px !default;
$p-tabs-item: $p-big !default;
// Tags
$bg-tag-dot: $bgc-body !default;
$bg-tag: $c-light !default;
$bdr-tag: $bdr-medium 0 0 $bdr-medium !default;
$c-tag: $c-text !default;
$lh-tag: 16px !default;
$m-tag: 0 $m-medium $m-medium 0 !default;
$p-tag: $p-medium !default;
$size-tag-dot: 5px !default;

View File

@ -0,0 +1,36 @@
/**
* BADGE
*
* Markup:
* -------
*
* <ul>
* <li class="badge badge--primary">Badge 1</li>
* <li class="badge badge--secondary badge--big">Badge 1</li>
* </ul>
*
*/
.badge {
background-color: $bg-badge;
border-radius: $bdr-badge;
color: $c-badge;
display: inline-block;
line-height: $lh-badge;
padding: $p-badge;
&--big {
font-size: $fz-badge-big;
}
&--small {
font-size: $fz-badge-small;
}
@each $type, $color in $badges-color-map {
&--#{$type} {
background-color: $color;
color: $c-badge-colored;
}
}
}

View File

@ -0,0 +1,149 @@
/**
* BUTTON
*
* Markup:
* -------
*
* <button class="button">Default</button>
* <button class="button button--big">Button big</button>
* <button class="button button--secondary">Button secondary</button>
*
*/
// reset default buttons
button {
background-color: transparent;
cursor: pointer;
}
.button {
background-color: $bg-button;
border: 0;
border-radius: $bdr-button;
color: $c-button;
display: inline-block;
font-family: $ff-button;
font-size: $fz-button;
font-weight: $fw-button;
line-height: $lh-button;
margin: $m-button;
padding: $p-button;
text-align: center;
text-decoration: none;
text-transform: $tt-button;
transition: opacity .2s ease-in-out;
white-space: nowrap;
&:focus,
&:hover,
&:active {
color: $c-button;
}
&:hover {
background-color: rgba($c-primary, .8);
color: $c-white;
cursor: pointer;
text-decoration: none;
}
&:active {
opacity: 1;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
// sizes
&--big {
font-size: $fz-button-big;
padding: $p-button-big;
}
&--small {
font-size: $fz-button-small;
padding: $p-button-small;
}
&--mobileFull {
@include media-breakpoint-down(md) {
margin-left: 0;
margin-right: 0;
width: 100%;
}
}
// colors
&--secondary {
background-color: $c-secondary;
color: $c-white;
&:hover {
background-color: rgba($c-secondary, .8);
color: $c-white;
}
}
&--white {
background-color: $c-white;
color: $c-primary;
&:hover {
background-color: rgba($c-white, .8);
color: rgba($c-primary, .8);
}
}
&--green {
background-color: $c-success;
color: $c-white;
&:hover {
background-color: rgba($c-success, .8);
color: $c-white;
}
}
&--red {
background-color: $c-error;
color: $c-white;
&:hover {
background-color: rgba($c-error, .8);
color: $c-white;
}
}
&--transparent {
background-color: transparent;
color: $c-button-transparent;
&:active,
&:hover,
&:focus {
background-color: transparent;
color: rgba($c-button-transparent, .8);
opacity: .8;
}
}
&--outlined {
background-color: transparent;
border: $bd-medium;
color: $c-button-outlined;
&:active,
&:hover,
&:focus {
background-color: transparent;
color: rgba($c-button-outlined, .8);
opacity: .8;
}
}
}

View File

@ -0,0 +1,398 @@
/**
* FORMS
*
* Markup:
* ---------
* <div class="input input-fullWidth">
* <input id="#" placeholder="#" type="text">
* </div>
*
* <div class="select select-fullWidth">
* <select name="#" id="#">
* <option>Option 1</option>
* <option>Option 2</option>
* </select>
* </div>
*
* <div class="textarea">
* <textarea id="#"></textarea>
* </div>
*
* <div class="radio">
* <input id="#" name="#" type="radio" value="">
* <label for="#">Radio button</label>
* </div>
*
* <div class="checkbox">
* <input id="#" name="#" type="checkbox" value="">
* <label for="#">Checkbox</label>
* </div>
*/
// prevent input default appearence on webkit
input,
textarea {
appearance: none;
}
label {
display: block;
font-weight: normal;
}
// Disable webkit background color
input:-webkit-autofill {
box-shadow: 0 0 0 1000px $bgc-form inset;
}
// Common styles
.textarea,
.input,
.select {
border: $bd-form;
border-radius: $bdr-form;
box-shadow: none;
display: inline-block;
font-weight: normal;
margin-bottom: 20px;
overflow: hidden;
:focus {
outline: none;
}
&.has-error {
background: $bgc-form-error;
border: $bd-form-error;
margin-bottom: 0; // we should display an error message
}
}
.select {
background-color: $bgc-form;
display: inline-block;
margin-right: $m-medium;
position: relative;
&:last-child {
margin-right: 0;
}
&-fullWidth {
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
}
select {
appearance: none;
background-color: transparent;
border: 0;
border-radius: 0;
color: $c-form;
display: block;
font-size: $fz-form;
line-height: $lh-medium;
margin: 0;
padding: $p-form;
padding-right: 30px;
transition: $trs-form;
width: 100%;
&:active,
&:focus {
background-color: $bgc-form-focus;
border: 0;
outline: none;
}
&::-ms-expand {
display: none;
}
}
&::after,
&::before {
background: $c-primary;
content: '';
display: block;
height: 2px;
margin-top: 2px;
position: absolute;
right: 5px;
top: 50%;
transform-origin: 1px;
width: 10px;
}
&::after {
transform: rotate(-135deg);
}
&::before {
transform: rotate(-45deg);
}
}
// textarea
.textarea {
background-color: $bgc-form;
padding: 0;
&-fullWidth {
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
}
textarea {
background: transparent;
border: 0;
color: $c-form;
display: block;
font-family: $ff-body;
font-size: $fz-form;
line-height: $lh-medium;
margin: 0;
min-height: $mih-form-textarea;
padding: $p-form;
transition: $trs-form;
width: 100%;
&::placeholder {
color: $c-form-placeholder;
}
&:focus,
&:active {
background-color: $bgc-form-focus;
border: 0;
outline: none;
}
}
}
.checkbox {
margin-bottom: $m-small;
position: relative;
input[type='checkbox'] {
display: none;
&:checked + label::after {
animation: checkboxAndRadioAnimation .25s;
content: '';
transform: scale(1) rotate(45deg);
}
& + label {
display: block;
overflow: hidden;
padding-left: 30px;
text-overflow: ellipsis;
white-space: nowrap;
&::before {
background-color: $bgc-form;
border: $bd-form;
border-radius: $bdr-medium;
content: '';
display: inline-block;
height: 20px;
left: 0;
margin-top: -10px;
position: absolute;
top: 50%;
width: 20px;
}
&::after {
border-bottom: 3px solid $c-primary;
border-right: 3px solid $c-primary;
display: block;
height: 12px;
left: 11px;
margin-left: -4px;
margin-top: -7px;
position: absolute;
top: 50%;
width: 7px;
z-index: 1;
}
}
}
}
.radio {
margin-bottom: $m-small;
position: relative;
input[type='radio'] {
display: none;
&:checked + label::after {
animation: checkboxAndRadioAnimation .25s;
content: '';
transform: scale(1) rotate(45deg);
}
& + label {
display: block;
overflow: hidden;
padding-left: 30px;
text-overflow: ellipsis;
white-space: nowrap;
&::before {
background-color: $bgc-form;
border: $bd-form;
border-radius: 20px;
content: '';
display: inline-block;
height: 20px;
left: 0;
margin-top: -10px;
position: absolute;
top: 50%;
width: 20px;
}
&::after {
background-color: $c-primary;
border-radius: 20px;
display: block;
height: 10px;
left: 11px;
margin-left: -6px;
margin-top: -6px;
position: absolute;
top: 13px;
width: 10px;
z-index: 1;
}
}
}
}
@keyframes checkboxAndRadioAnimation {
0% {
transform: scale(0) rotate(45deg);
}
50% {
transform: scale(1.5) rotate(45deg);
}
100% {
transform: scale(1) rotate(45deg);
}
}
.input {
background-color: $bgc-form;
margin-right: 10px;
padding: 0;
position: relative;
:focus,
:active {
background-color: $bgc-form-focus;
border-radius: $bdr-form;
}
input {
background: transparent;
border: 0;
box-shadow: none;
color: $c-form;
font-size: $fz-form;
line-height: $lh-medium;
margin: 0;
outline: none;
padding: $p-form;
width: 100%;
&::placeholder {
color: $c-form-placeholder;
}
}
&-withIcon {
input {
padding-right: $size-form-input-icon * 2;
}
}
&-icon {
fill: $c-form-input-icon;
height: $size-form-input-icon;
margin-top: -$size-form-input-icon / 2;
position: absolute;
right: $m-medium;
top: 50%;
width: $size-form-input-icon;
}
// input variations
&-fullWidth {
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
}
}
/**
* FORM COLLAPSED (items in row without gap between them)
*
* Markup:
* -------
*
* <div class="formCollapsed">
* <div class="input formCollapsed-item formCollapsed-itemPrimary">
* <input id="name" placeholder="Klingon search" type="text" />
* </div>
* <div class="select formCollapsed-item">
* <select name="country-code" id="country-code">
* <option value="AO"> Angola</option>
* </select>
* </div>
* <button class="formCollapsed-item button button-primary">
* Search
* </button>
* </div>
**/
.formCollapsed {
display: flex;
margin-bottom: 20px;
&-item {
border-radius: 0 !important;
margin: 0 !important;
&:first-child {
border-bottom-left-radius: $bdr-form !important;
border-top-left-radius: $bdr-form !important;
}
&:last-child {
border-bottom-right-radius: $bdr-form !important;
border-top-right-radius: $bdr-form !important;
}
&:not(:last-child) {
border-right: 0;
}
&Primary {
flex: 1;
}
}
}

View File

@ -0,0 +1,57 @@
/**
* LOADING BAR
*
* Markup:
* ---------
* <div class="loadingBar"></div>
*
*/
.loadingBar {
height: $h-loading-bar;
left: 0;
overflow: hidden;
position: fixed;
right: 0;
top: 0;
width: 100%;
z-index: $z-loading-bar;
&::before {
animation: loading 2s linear infinite;
background-color: $c-primary;
content: '';
display: block;
height: $h-loading-bar;
left: -$w-loading-bar;
position: absolute;
width: $w-loading-bar;
}
}
@keyframes loading {
from {
left: -$w-loading-bar;
width: 30%;
}
50% {
width: 30%;
}
70% {
width: 70%;
}
80% {
left: 50%;
}
95% {
left: 120%;
}
to {
left: 100%;
}
}

View File

@ -0,0 +1,139 @@
/**
* LOADING SPINNER
*
* Markup:
* ---------
* <div class='loadingSpinner'>
* <span class='loadingSpinner-inner'></span>
* <span class='loadingSpinner-inner'></span>
* <span class='loadingSpinner-inner'></span>
* <span class='loadingSpinner-inner'></span>
* </div>
*
*/
.loadingSpinner {
animation: rotateLoader 4s infinite;
animation-timing-function: ease-in-out;
display: block;
height: $size-loading-spinner;
left: 50%;
margin-left: -$size-loading-spinner / 2;
margin-top: -$size-loading-spinner / 2;
position: fixed;
top: 50%;
width: $size-loading-spinner;
z-index: $z-loading-spinner;
&-inner {
animation-timing-function: ease-in-out;
background-color: $bg-loading-spinner;
border-radius: 100%;
display: block;
height: $size-loading-spinner-inner;
position: absolute;
width: $size-loading-spinner-inner;
&:nth-child(1) {
animation: translateBall1 1s infinite;
left: 0;
top: 0;
transform: translate3d($size-loading-spinner-inner / 2, $size-loading-spinner-inner / 2, 0);
}
&:nth-child(2) {
animation: translateBall2 1s infinite;
right: 0;
top: 0;
}
&:nth-child(3) {
animation: translateBall3 1s infinite;
bottom: 0;
right: 0;
}
&:nth-child(4) {
animation: translateBall4 1s infinite;
bottom: 0;
left: 0;
}
}
}
@keyframes rotateLoader {
0% {
transform: rotate(0);
}
25% {
transform: rotate(90deg);
}
50% {
transform: rotate(180deg);
}
75% {
transform: rotate(270deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes translateBall1 {
0% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d($size-loading-spinner-inner / 2, $size-loading-spinner-inner / 2, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
@keyframes translateBall2 {
0% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d(-$size-loading-spinner-inner / 2, $size-loading-spinner-inner / 2, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
@keyframes translateBall3 {
0% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d(-$size-loading-spinner-inner / 2, -$size-loading-spinner-inner / 2, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
@keyframes translateBall4 {
0% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d($size-loading-spinner-inner / 2, -$size-loading-spinner-inner / 2, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}

View File

@ -0,0 +1,31 @@
/**
* NOTIFICATION
*
* Markup:
* -------
*
* <div class="notification notification--sucess">Success notification</div>
* <div class="notification notification--info">Success info</div>
* <div class="notification notification--error">Success error</div>
* <div class="notification notification--warning">Success warning</div>
*
*/
.notification {
border-radius: $bdr-notification;
color: $c-notification;
margin-bottom: $mb-notification;
padding: $p-notification;
p {
&:last-child {
margin-bottom: 0;
}
}
@each $type, $color in $c-map {
&--#{$type} {
background-color: $color;
}
}
}

View File

@ -0,0 +1,45 @@
/**
* PAGINATOR
*
* Markup:
* -------
*
* <ul class="paginator">
* <li class="paginator-item">
* <a href="#" class="paginator-itemLink">< Prev</a>
* </li>
* <li class="paginator-item">
* <a href="#" class="paginator-itemLink">1</a>
* </li>
* <li class="paginator-item">
* <a href="#" class="paginator-itemLink is-active">2</a>
* </li>
* <li class="paginator-item">
* <a href="#" class="paginator-itemLink">3</a>
* </li>
* <li class="paginator-item">
* <a href="#" class="paginator-itemLink">Next ></a>
* </li>
* </ul>
*/
.paginator {
&-item {
display: inline-block;
margin-right: $mr-paginator-item;
&Link {
background-color: $bg-paginator-item;
border-radius: $bdr-paginator-item;
display: block;
padding: $p-paginator-item;
&.is-active {
background-color: $bg-paginator-item-active;
color: $c-paginator-item-active;
cursor: default;
}
}
}
}

View File

@ -0,0 +1,123 @@
/**
* TABLE
*
* Markup:
* -------
*
* <table class="table">
* <tr>
* <th>First column</th>
* <th>Second column</th>
* <th>Third column</th>
* </tr>
* <tr>
* <td="First column">Blue</td>
* <td="Second column">One</td>
* <td="Third column">My life fades</td>
* </tr>
* </table>
*
*/
.table {
background-color: $bgc-table;
border: $bd-table;
border-collapse: collapse;
color: $c-table-text;
max-width: $mwi-table;
width: $w-table;
th,
td {
border-bottom: $bd-table;
padding: $p-table-cell;
position: relative;
}
thead {
border-bottom: $bd-table;
}
th {
background-color: $bgc-table-header;
color: $c-table-heading;
font-size: $fz-table-heading;
font-weight: $fw-table-heading;
padding: $p-table-heading;
white-space: nowrap;
}
}
/**
* TABLE RESPONSIVE
*
* Markup:
* -------
*
* <table class="table table--responsive">
* <tr>
* <th>First column</th>
* <th>Second column</th>
* <th>Third column</th>
* </tr>
* <tr>
* <td data-th="First column">Blue</td>
* <td data-th="Second column">One</td>
* <td data-th="Third column">My life fades</td>
* </tr>
* <tr>
* <td data-th="First column">Green</td>
* <td data-th="Second column">Two</td>
* <td data-th="Third column">
* when the world was powered by the black fuel... and the desert
* sprouted great cities of pipe and steel.
* </td>
* </tr>
* <tr>
* <td data-th="First column">Yellow</td>
* <td data-th="Second column">Three</td>
* <td data-th="Third column">
* A whirlwind of looting, a firestorm of fear.
* </td>
* </tr>
* </table>
*
*/
.table--responsive {
th {
display: none;
}
td {
@include media-breakpoint-down(sm) {
border: 0;
}
display: block;
&::before {
@include media-breakpoint-up(sm) {
display: none;
}
color: $c-table-heading;
content: attr(data-th)': ';
display: block;
font-weight: normal;
}
&:first-child {
border-top: $bd-table;
}
}
th,
td {
@include media-breakpoint-up(sm) {
border-top: $bd-table;
display: table-cell;
}
text-align: left;
}
}

View File

@ -0,0 +1,37 @@
/**
* TABS
*
* Markup:
* -------
*
* <div class="tabs">
* <a href="#" title="#" class="tabs-item">[...]</a>
* <a href="#" title="#" class="tabs-item is-selected">[...]</a>
* </div>
*
*/
.tabs {
border-bottom: $bdb-tabs;
text-align: center;
&-item {
border-bottom: $bdb-tabs-item;
color: $c-tabs-item;
display: inline-block;
margin: $m-tabs-item;
min-width: $miw-tabs-item;
padding: $p-tabs-item;
position: relative;
&:hover {
color: $c-tabs-item-hover;
text-decoration: none;
}
&.is-selected {
border-bottom: $bdb-tabs-item-selected;
color: $c-tabs-item-selected;
}
}
}

View File

@ -0,0 +1,62 @@
/**
* TAG
*
* Markup:
* -------
*
* <ul>
* <li class="tag">Fantasy</li>
* <li class="tag">Fiction</li>
* <li class="tag">Contemporary</li>
* </ul>
*
* <ul>
* <li class="tag">
* <a href="#">Fantasy</a>
* </li>
* <li class="tag">
* <a href="#">Fiction</a>
* </li>
* <li class="tag">
* <a href="#">Contemporary</a>
* </li>
* </ul>
*
*/
.tag {
background-color: $bg-tag;
border-radius: $bdr-tag;
color: $c-tag;
display: inline-block;
line-height: $lh-tag;
margin: $m-tag;
padding: $p-tag;
position: relative;
// triangle arrow
&::before {
border-bottom: ($lh-tag / 2) + $p-tag solid transparent;
border-left: $p-tag solid $bg-tag;
border-top: ($lh-tag / 2) + $p-tag solid transparent;
content: '';
height: 0;
position: absolute;
right: -$p-tag;
top: 0;
width: 0;
}
// dot
&::after {
background: $bg-tag-dot;
border-radius: 100%;
content: '';
height: $size-tag-dot;
margin-top: -$size-tag-dot / 2;
position: absolute;
right: -$size-tag-dot / 2;
top: 50%;
width: $size-tag-dot;
}
}

2072
public/scss/custom.scss Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
// Framework grid generation
//
// Used only by Bootstrap to generate the correct number of grid classes given
// any value of `$grid-columns`.
@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {
// Common properties for all breakpoints
%grid-column {
position: relative;
width: 100%;
min-height: 1px; // Prevent columns from collapsing when empty
padding-right: ($gutter / 2);
padding-left: ($gutter / 2);
}
@each $breakpoint in map-keys($breakpoints) {
$infix: breakpoint-infix($breakpoint, $breakpoints);
// Allow columns to stretch full width below their breakpoints
@for $i from 1 through $columns {
.col#{$infix}-#{$i} {
@extend %grid-column;
}
}
.col#{$infix},
.col#{$infix}-auto {
@extend %grid-column;
}
@include media-breakpoint-up($breakpoint, $breakpoints) {
// Provide basic `.col-{bp}` classes for equal-width flexbox columns
.col#{$infix} {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
}
.col#{$infix}-auto {
flex: 0 0 auto;
width: auto;
max-width: none; // Reset earlier grid tiers
}
@for $i from 1 through $columns {
.col#{$infix}-#{$i} {
@include make-col($i, $columns);
}
}
.order#{$infix}-first {
order: -1;
}
@for $i from 1 through $columns {
.order#{$infix}-#{$i} {
order: $i;
}
}
// `$columns - 1` because offsetting by the width of an entire row isn't possible
@for $i from 0 through ($columns - 1) {
@if not ($infix == "" and $i == 0) { // Avoid emitting useless .offset-0
.offset#{$infix}-#{$i} {
@include make-col-offset($i, $columns);
}
}
}
}
}
}

View File

@ -0,0 +1,52 @@
/// Grid system
//
// Generate semantic grid columns with these mixins.
@mixin make-container() {
width: 100%;
padding-right: ($grid-gutter-width / 2);
padding-left: ($grid-gutter-width / 2);
margin-right: auto;
margin-left: auto;
}
// For each breakpoint, define the maximum width of the container in a media query
@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {
@each $breakpoint, $container-max-width in $max-widths {
@include media-breakpoint-up($breakpoint, $breakpoints) {
max-width: $container-max-width;
}
}
}
@mixin make-row() {
display: flex;
flex-wrap: wrap;
margin-right: ($grid-gutter-width / -2);
margin-left: ($grid-gutter-width / -2);
}
@mixin make-col-ready() {
position: relative;
// Prevent columns from becoming too narrow when at smaller grid tiers by
// always setting `width: 100%;`. This works because we use `flex` values
// later on to override this initial width.
width: 100%;
min-height: 1px; // Prevent collapsing
padding-right: ($grid-gutter-width / 2);
padding-left: ($grid-gutter-width / 2);
}
@mixin make-col($size, $columns: $grid-columns) {
flex: 0 0 percentage($size / $columns);
// Add a `max-width` to ensure content within each column does not blow out
// the width of the column. Applies to IE10+ and Firefox. Chrome and Safari
// do not appear to require this.
max-width: percentage($size / $columns);
}
@mixin make-col-offset($size, $columns: $grid-columns) {
$num: $size / $columns;
margin-left: if($num == 0, 0, percentage($num));
}

34
public/scss/index.scss Normal file
View File

@ -0,0 +1,34 @@
@charset 'UTF-8';
// Settings
@import 'settings';
@import 'custom';
// Utils
@import 'utils/breakpoints';
@import 'utils/reset';
@import 'utils/default';
@import 'utils/background';
@import 'utils/border';
@import 'utils/aligner';
@import 'utils/helpers';
@import 'utils/layout';
@import 'utils/typography';
// Components
@import 'components/badge';
@import 'components/buttons';
@import 'components/forms';
@import 'components/loading-bar';
@import 'components/loading-spinner';
@import 'components/notification';
@import 'components/paginator';
@import 'components/table';
@import 'components/tabs';
@import 'components/tag';
// Twitter bootstrap's grid v4.1.0
// More info: https://getbootstrap.com/examples/grid/
@import 'grid/grid-framework';
@import 'grid/grid';
@import 'utils/grid';

View File

@ -0,0 +1,48 @@
/**
* ALIGNERS
*/
// container
.aligner {
display: flex;
&--spaceBetween {
justify-content: space-between;
width: 100%;
}
&--spaceAround {
justify-content: space-around;
width: 100%;
}
&--centerVertical {
align-items: center;
}
&--centerHoritzontal {
justify-content: center;
}
&--contentStart {
justify-content: flex-start;
}
&--contentEnd {
justify-content: flex-end;
}
}
// item
.aligner--itemTop {
align-self: flex-start;
}
.aligner--itemBottom {
align-self: flex-end;
}
.flex-grow, // deprecated
.aligner--grow {
flex-grow: 1;
}

View File

@ -0,0 +1,11 @@
/**
* BACKGROUND
*/
.bg {
@each $type, $color in $c-map {
&-#{$type} {
background-color: $color;
}
}
}

View File

@ -0,0 +1,23 @@
/**
* BORDER
*/
.border {
border: $bd-medium;
}
.border-bottom {
border-bottom: $bd-medium;
}
.border-left {
border-left: $bd-medium;
}
.border-right {
border-right: $bd-medium;
}
.border-top {
border-top: $bd-medium;
}

View File

@ -0,0 +1,119 @@
// Breakpoint viewport sizes and media queries.
//
// Breakpoints are defined as a map of (name: minimum width), order from small to large:
//
// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)
//
// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.
// Name of the next breakpoint, or null for the last breakpoint.
//
// >> breakpoint-next(sm)
// md
// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// md
// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))
// md
@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
$n: index($breakpoint-names, $name);
@return if($n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);
}
// Minimum breakpoint width. Null for the smallest (first) breakpoint.
//
// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// 576px
@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {
$min: map-get($breakpoints, $name);
@return if($min != 0, $min, null);
}
// Maximum breakpoint width. Null for the largest (last) breakpoint.
// The maximum value is calculated as the minimum of the next one less 0.1.
//
// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// 767px
@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {
$next: breakpoint-next($name, $breakpoints);
@return if($next, breakpoint-min($next, $breakpoints) - 1px, null);
}
// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash infront.
// Useful for making responsive utilities.
//
// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// "" (Returns a blank string)
// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))
// "-sm"
@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {
@return if(breakpoint-min($name, $breakpoints) == null, "", "-#{$name}");
}
// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.
// Makes the @content apply to the given breakpoint and wider.
@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints);
@if $min {
@media (min-width: $min) {
@content;
}
} @else {
@content;
}
}
// Media of at most the maximum breakpoint width. No query for the largest breakpoint.
// Makes the @content apply to the given breakpoint and narrower.
@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {
$max: breakpoint-max($name, $breakpoints);
@if $max {
@media (max-width: $max) {
@content;
}
} @else {
@content;
}
}
// Media that spans multiple breakpoint widths.
// Makes the @content apply between the min and max breakpoints
@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($lower, $breakpoints);
$max: breakpoint-max($upper, $breakpoints);
@if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) {
@content;
}
} @else if $max == null {
@include media-breakpoint-up($lower) {
@content;
}
} @else if $min == null {
@include media-breakpoint-down($upper) {
@content;
}
}
}
// Media between the breakpoint's minimum and maximum widths.
// No minimum for the smallest breakpoint, and no maximum for the largest one.
// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.
@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {
$min: breakpoint-min($name, $breakpoints);
$max: breakpoint-max($name, $breakpoints);
@if $min != null and $max != null {
@media (min-width: $min) and (max-width: $max) {
@content;
}
} @else if $max == null {
@include media-breakpoint-up($name) {
@content;
}
} @else if $min == null {
@include media-breakpoint-down($name) {
@content;
}
}
}

View File

@ -0,0 +1,39 @@
/**
* MAIN RULES
*/
*,
*::after,
*::before {
box-sizing: border-box;
outline: none;
}
body {
background-color: $bgc-body;
min-height: 100%;
overflow-x: hidden;
position: relative;
}
p {
font-weight: normal;
margin-bottom: $mb-p;
}
img {
max-width: 100%;
}
strong {
font-weight: $fw-strong;
}
ul {
margin-bottom: $mb-ul;
}
li {
list-style: none;
margin-bottom: $mb-li;
}

View File

@ -0,0 +1,52 @@
// Container widths
//
// Set the container width, and override it for fixed navbars in media queries.
@if $enable-grid-classes {
.container {
@include make-container;
@include make-container-max-widths;
}
}
// Fluid container
//
// Utilizes the mixin meant for fixed width containers, but with 100% width for
// fluid, full width layouts.
@if $enable-grid-classes {
.container-fluid {
@include make-container;
}
}
// Row
//
// Rows contain and clear the floats of your columns.
@if $enable-grid-classes {
.row {
@include make-row;
}
// Remove the negative margin from default .row, then the horizontal padding
// from all immediate children columns (to prevent runaway style inheritance).
.no-gutters {
margin-left: 0;
margin-right: 0;
> .col,
> [class*='col-'] {
padding-left: 0;
padding-right: 0;
}
}
}
// Columns
//
// Common styles for small and large grid columns
@if $enable-grid-classes {
@include make-grid-columns;
}

View File

@ -0,0 +1,126 @@
/**
* FLOATS
*/
.fleft {
float: left;
}
.fright {
float: right;
}
.clearfix {
::after {
clear: both;
content: '';
display: table;
}
}
/**
* MARGINS
*/
.m-xsmall {
margin: $m-xsmall;
}
.mb-xsmall {
margin-bottom: $m-xsmall;
}
.m-small {
margin: $m-small;
}
.mb-small {
margin-bottom: $m-small;
}
.m-medium {
margin: $m-medium;
}
.mb-medium {
margin-bottom: $m-medium;
}
.m-big {
margin: $m-big;
}
.mb-big {
margin-bottom: $m-big;
}
.m-huge {
margin: $m-huge;
}
.mb-huge {
margin-bottom: $m-huge;
}
.m-none {
margin: 0 !important;
}
/**
* PADDINGS
*/
.p-small {
padding: $p-small;
}
.pb-small {
padding-bottom: $p-small;
}
.p-medium {
padding: $p-medium;
}
.pb-medium {
padding-bottom: $p-medium;
}
.p-big {
padding: $p-big;
}
.pb-big {
padding-bottom: $p-big;
}
.p-huge {
padding: $p-huge;
}
.pb-huge {
padding-bottom: $p-huge;
}
/**
* OTHERS
*/
.no-wrap {
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.opacity-low {
opacity: .5;
}
.rounded-corners {
border-radius: $bdr-rounded-corners;
}
.rounded {
border-radius: 100%;
}

View File

@ -0,0 +1,51 @@
/**
* LAYOUT
*/
.section {
@include media-breakpoint-up(md) {
padding-bottom: $p-huge * 2;
padding-top: $p-huge * 2;
}
padding-bottom: $p-huge;
padding-top: $p-huge;
& + & {
padding-top: 0;
}
}
.container {
@include media-breakpoint-up(md) {
padding-left: $w-gutter;
padding-right: $w-gutter;
}
background-color: $bgc-container;
margin: 0 auto;
max-width: $mwi-container;
padding-left: $w-gutter / 2;
padding-right: $w-gutter / 2;
width: 100%;
}
.container-medium {
@include media-breakpoint-up(md) {
padding-left: $w-gutter;
padding-right: $w-gutter;
}
margin: 0 auto;
max-width: $mwi-container-medium;
padding-left: $w-gutter / 2;
padding-right: $w-gutter / 2;
}
.container-small {
@include media-breakpoint-up(md) {
padding-left: $w-gutter;
padding-right: $w-gutter;
}
margin: 0 auto;
max-width: $mwi-container-small;
padding-left: $w-gutter / 2;
padding-right: $w-gutter / 2;
}

View File

@ -0,0 +1,34 @@
/**
* Reset
*/
@if $enable-reset-defaults {
html,body,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,figure,footer,header,hgroup,menu,nav,section,time,mark,audio,video {
background: transparent;
border: 0;
font-size: 100%;
margin: 0;
outline: 0;
padding: 0;
vertical-align: baseline;
}
article,
aside,
figure,
footer,
header,
main,
nav,
section {
display: block;
}
* {
&,
&:before,
&:after {
box-sizing: border-box;
}
}
}

View File

@ -0,0 +1,124 @@
/**
* TYPOGRAPHY
*/
body {
color: $c-body;
font-family: $ff-body;
font-size: $fz-body;
font-weight: $fw-body;
line-height: $lh-body;
}
a {
color: $c-link;
text-decoration: none;
&:hover {
color: $c-link-hover;
}
&:focus {
color: $c-primary;
}
}
// Sizes
.text {
&-huge, &-big, &-medium {
margin-bottom: 1em;
}
&-huge {
font-size: $fz-huge;
line-height: $lh-huge;
}
&-big {
font-size: $fz-big;
line-height: $lh-big;
}
&-medium {
font-size: $fz-medium;
line-height: $lh-medium;
}
&-small {
font-size: $fz-small;
line-height: $lh-small;
}
// set default font state.
&-body {
font-size: $fz-body;
line-height: $lh-body;
}
}
// Colors
.text {
@each $type, $color in $c-map {
&-#{$type} {
color: $color;
}
}
}
// Styles
.text-light {
font-weight: $fw-light;
}
.text-normal {
font-weight: $fw-normal;
}
.text-lineThrough {
text-decoration: line-through;
}
.text-italic {
font-style: italic;
}
.text-underline {
text-decoration: underline;
}
.text-uppercase {
text-transform: uppercase;
}
// titles after titles
.text-withSubtitle {
margin-bottom: 0 !important;
+ .text-huge,
+ .text-big,
+ .text-medium,
+ .text-small, {
margin-top: .5em;
}
}
h1,
h2,
h3,
h4 {
font-weight: $fw-headings;
}
// Aligners
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}

76
router/api.js Normal file
View File

@ -0,0 +1,76 @@
const crypto = require('crypto');
const axios = require('axios');
const pandocc = require('../compile_pandoc.js')('otm');
module.exports = (fastify, opts, done) => {
fastify.get('/flight/:id', async (req,reply) => {
const ENDPOINT = 'https://www.flightradar24.com/v1/search/web/find';
const FORMAT = '-';
if(req.params.id){
axios.get(ENDPOINT,{
params: {
format: FORMAT,
query: req.params.id,
limit:16,
type: 'schedule'
}
}).then(res=>{reply.send(res.data)});
}else{
reply.send([]);
}
});
fastify.get('/:id', (req, reply) => {
if(req.params.id == undefined){
reply.code(400).send({error:"No ID query parameter"});
} else {
const { level } = fastify
level.get(req.params.id, (err, val) => {
if(err){
console.warn(err);
reply.send({name:"New Journey", main:[]});
} else {
reply.send(JSON.parse(val));
}
});
}
});
fastify.post('/:id', (req, reply) => {
if(req.params.id == undefined){
reply.code(400).send({error:"No ID query parameter"});
} else {
const { level } = fastify
level.put(req.params.id, JSON.stringify(req.body), (err) => {
if(err){
console.warn(err);
reply.code(500).send({error:"Error with DB"});
} else {
reply.send({content:"ok"});
}
});
}
});
fastify.delete('/:id',(req, reply) => {
if(req.params.id == undefined){
reply.code(400).send({error:"No ID query parameter"});
} else {
const { level } = fastify
level.delete(req.params.id,(err) => {
if(err){
console.warn(err);
reply.code(500).send({error:"Error with DB"});
} else {
reply.send({content:"ok"});
}
});
}
});
done();
};

90
server.js Normal file
View File

@ -0,0 +1,90 @@
const fastify = require('fastify')();//{ logger: true });
const path = require('path');
const crypto = require('crypto');
const csass = require("./compile_sass").compileSassMain();
const static_opts = {
root: path.join(__dirname, 'public'),
prefix: '/public/',
};
const pov_opts = {
engine: {
ejs: require('ejs')
}
};
fastify.hashgen = (str)=>{
return crypto.createHash('md5').update('CHELCEL_PRE_SALT_##'+str+'##_SALT_POST_CHELCEL').digest('hex');
};
fastify.cookiegen = (request, type, file_hash)=>{
let tk = {};
if(request.cookies.token ){
tk = fastify.jwt.decode(request.cookies.token);
}
if(tk[type]== undefined){
tk[type]=[];
}
if(tk[type].indexOf(file_hash)== -1){
tk[type].push(file_hash);
}
const token = fastify.jwt.sign(tk)
return token;
}
fastify.register(require('fastify-static'), static_opts);
fastify.register(require('fastify-cookie'));
//fastify.register(require('fastify-websocket'));
fastify.register(require('point-of-view'), pov_opts);
fastify.register(require('fastify-leveldb'), {
name: 'db'
}, err => {
if (err) throw err
});
fastify.get('/', (req, reply) => {
reply.view('/template/home.html');
});
fastify.get('/dbg', (req, reply) => {
reply.view('/template/templates.html');
});
fastify.get('/:id', (req, reply) => {
try{
const ec = parseInt(req.params.id);
switch(ec){
case 400:
case 401:
case 402:
case 403:
case 404:
case 405:
reply.code(ec).send("Client Error");
break;
case 500:
reply.code(ec).send("Internal Error");
break;
default:
throw undefined;
}
}catch(e){
reply.view('/template/journey.html');
}
});
fastify.register(require('./router/api'), { prefix: '/api' });
fastify.listen(8080,'0.0.0.0' ,(err,address) => {
if (err) throw err;
console.log("Listening on", address);
});

117
template/default.html Normal file
View File

@ -0,0 +1,117 @@
<!DOCTYPE html>
<html$if(lang)$ lang="$lang$"$endif$ >
<head>
<meta charset="utf-8">
<meta name="generator" content="pandoc">
<link rel='shortcut icon' href='/public/img/helcel.ico' type='image/x-icon'/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
$for(author-meta)$
<meta name="author" content="$author-meta$">
$endfor$
$if(date-meta)$
<meta name="dcterms.date" content="$date-meta$">
$endif$
<title>$if(title-prefix)$$title-prefix$ - $endif$$pagetitle$</title>
<style type="text/css">code{white-space: pre;}</style>
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
$if(quotes)$
<style type="text/css">q { quotes: "“" "”" "" ""; }</style>
$endif$
$for(css)$
<link rel="stylesheet" href="$css$">
$endfor$
$for(header-includes)$
$header-includes$
$endfor$
</head>
<body>
<header itemscope itemtype="https://schema.org/Person" class="$if(photo)$with-photo$endif$">
<div id="title">
$if(qrcode)$
<img class="logo" src="$qrcode$" />
$endif$
<h1 class="fullname">
<span itemprop="givenName">$firstname$</span>
<span itemprop="familyName">$lastname$</span>
</h1>
<h2 class="title">$description$</h2>
</div>
$if(photo)$
<img class="portrait" src="$photo$"></div>
$endif$
<ul class="details">
<!-- phone -->
$if(phone)$
$if(settings.protect-phone)$
$if(privatecv)$
<li><a class='phone' href="phoneto:$phone$">$phone$</a></li>
$endif$
$else$
<li><a class='phone' href="phoneto:$phone$">$phone$</a></li>
$endif$
$endif$
<!-- mobile -->
$if(mobile)$
$if(settings.protect-mobile)$
$if(privatecv)$
<li><a class='mobile' href="phoneto:$mobile$">$mobile$</a></li>
$endif$
$else$
<li><a class='mobile' href="phoneto:$mobile$">$mobile$</a></li>
$endif$
$endif$
<!-- email -->
$if(email)$
$if(settings.protect-email)$
$if(privatecv)$
<li><a href="mailto:$email$">$email$</a></li>
$endif$
$else$
<li><a href="mailto:$email$">$email$</a></li>
$endif$
$endif$
<!-- homepage -->
$if(homepage)$
<li><a href="$homepage$" itemprop="url" title="homepage">$homepage$</a></li>
$endif$
<!--if(address)-->
$if(address)$
$if(settings.protect-address)$
$if(privatecv)$
<li itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
$if(address.city)$
<span itemprop="addressLocality">$address.city$</span>
$endif$
$if(address.country)$
<span itemprop="addressCountry">$address.country$</span>
$endif$
</li>
$endif$
$else$
<li itemprop="address" itemscope itemtype="http://schema.org/PostalAddress">
$if(address.city)$
<span itemprop="addressLocality">$address.city$</span>
$endif$
$if(address.country)$
<span itemprop="addressCountry">$address.country$</span>
$endif$
</li>
$endif$
$endif$
<!--endif-->
</ul>
</header>
$for(include-before)$
$include-before$
$endfor$
$body$
$for(include-after)$
$include-after$
$endfor$
<footer>
$footer$
</footer>
</body>
</html>

139
template/home.html Normal file
View File

@ -0,0 +1,139 @@
<html>
<head>
<title>Helcel-OTM</title>
<link rel='shortcut icon' href='/public/img/helcel.ico' type='image/x-icon'/>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700,300" type="text/css">
<link rel='stylesheet' href='/public/css/index.css'/>
<script src="/public/js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue2-datepicker/index.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vue2-datepicker/index.css">
<link rel="stylesheet" href="//unpkg.com/leaflet/dist/leaflet.css" />
<script src="//unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="//unpkg.com/vue2-leaflet"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script>
<script src="https://unpkg.com/vue-multiselect"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-multiselect/dist/vue-multiselect.min.css">
</head>
<body>
<main id="app">
<!--<header class="header js-header">
<div class="header-inner container">
<a href="/" class="header-logo text-dark">
<img class="header-logoImage" src="/public/img/helcel.png" alt="Helcel logo" width="40">
<span class="hide-small">Helcel</span>
</a>
<ul class="header-nav">
<li class="header-navItem"><a class="text-dark" href="/">OTM</a></li>
</ul>
</div>
</header>-->
<div class="container">
<section class="mb-big">
<div class="text-center">
<img class="main-logo mb-medium" src="/public/img/helcel.png" alt="Helcel logo">
<div>
<h1 class="text-huge text-withSubtitle">Open Tourism Map</h1>
<h2 class="text-big text-gray">Collaborative Holiday Planner</h2>
</div>
<p id="js-header-waypoint" class="m-none">
<a class="button button--primary button--mobileFull" href="#go">Get started</a>
</p>
</div>
</section>
</div>
<div class="bg-dark">
<div class="container">
<div class="row text-center">
<div class="col-12 col-sm-3">
<div class="section">
<img src="/public/img/lightweight.png" alt="Lightweight" width="118"> <br>
<h2 class="text-withSubtitle text-big text-white">Lightweight <br>
<span class="text-medium text-gray">Powered By<br>Fastify & Sierra</span></h2>
</div>
</div>
<div class="col-12 col-sm-4">
<div class="section">
<img src="/public/img/customizable.png" alt="Customizable" width="118"> <br>
<h2 class="text-withSubtitle text-big text-white">Customizable<br>
<span class="text-medium text-gray">Many Templates<br>to choose from</span>
</h2>
</div>
</div>
<div class="col-12 col-sm-4">
<div class="section">
<h2 class="text-withSubtitle text-big text-white">
<img src="/public/img/opensource.png" alt="Open Source" width="118"> <br>
FOSS<br>
<span class="text-medium text-gray">:-)</span>
</h2>
</div>
</div>
</div>
</div>
</div>
<div id="go" class="container-medium section">
<h2 class="text-big">Your journey</h2>
<p>Browse hotels, restaurants and attractions,.... <br> Select and plan the varying elements of your journey</p>
<div class="aligner aligner--contentEnd">
<div class="input">
<input id="journey_id" v-model="journey_id" placeholder="ID" type="text">
</div>
<button class="button button--primary button--mobileFull" v-on:click="start_journey">Start the journey</button>
</div>
</div>
<div>
<div class="section bg-dark">
<div class="container text-center">
<h3 class="text-huge text-white text-withSubtitle">Developed entirely with Sierra library selectors</h3>
<h4 class="text-big text-gray">Just define your custom SCSS variables and compile</h4>
<p>
<p><a href="https://sierra-library.github.io/" class="button button--secondary button--medium button--mobileFull">Documentation</a> <a href="https://github.com/sierra-library/sierra-library.github.io/tree/master/examples/example1" class="button button--outlined button--medium button--mobileFull">View code</a></p>
</p>
</div>
</div>
</div>
<div class="container-medium section">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<div class="aligner aligner--contentEnd">
<a href="https://github.com/sierra-library/sierra" class="button button--primary button--mobileFull">Visit Github Page</a>
</div>
</div>
</main>
<script src="/public/js/main.js" type="text/javascript" charset="utf-8"></script>
<footer class="bg-dark">
<div class="container">
<div class="section text-center text-small">
<p class="text-white">
<img src="/public/img/helcel.png" alt="helcel logo" width="100"><br><br>
Built with &nbsp;&nbsp; by Helcel <br><span class="text-small text-gray">v0.0.1</span>
</p>
<p class="text-gray"><a href="https://git.helcel.net">Helcel Git</a></p>
</div>
</div>
</footer>
</body>
</html>

320
template/journey.html Normal file
View File

@ -0,0 +1,320 @@
<html>
<head>
<title>Helcel-OTM</title>
<link rel='shortcut icon' href='/public/img/helcel.ico' type='image/x-icon'/>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700,300" type="text/css">
<link rel='stylesheet' href='/public/css/index.css'/>
<script src="/public/js/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vue2-datepicker/index.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vue2-datepicker/index.css">
<link rel="stylesheet" href="//unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script src="https://unpkg.com/vue2-leaflet"></script>
<script src="https://unpkg.com/leaflet.awesome-markers/dist/leaflet.awesome-markers.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet.awesome-markers/dist/leaflet.awesome-markers.css">
<link rel="stylesheet" href="https://unpkg.com/@fortawesome/fontawesome-free@5.15.3/css/all.min.css">
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script>
<script src="https://unpkg.com/vue-multiselect"></script>
<script src="//cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>
<script src="https://unpkg.com/vuedraggable/dist/vuedraggable.umd.min.js"></script>
<script src="https://unpkg.com/vue-textarea-autosize/dist/vue-textarea-autosize.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/vue-multiselect/dist/vue-multiselect.min.css">
</head>
<body>
<main id="app">
<header class="header">
<div class="header-inner container">
<a href="/" class="header-logo text-dark">
<img class="header-logoImage" src="/public/img/helcel.png" alt="Helcel logo" width="40">
<span class="hide-small">HOTM</span>
</a>
<div class="input input-invis"><input class="small" v-model="journey_data.name" type="text" /></div>
<ul class="header-nav">
<div class="col-sm-5"><button class="button button--primary button--mobileFull" v-on:click="first_step">Main</button></div>
<div class="col-sm-5"><button class="button button--primary button--mobileFull" v-on:click="next_step">Next</button></div>
</ul>
</div>
</header>
<div v-if="journey_step==-1">
<div v-for="(item,idx) in journey_data.main" :class="idx%2===0 ? 'bg-dark text-white' : ''" v-cloak>
<div class="container section">
<div class="row text-center">
<div class="col-sm-1"><button class="button button--secondary button--mobileFull" v-on:click="rm_section(idx)">X</button></div>
<div class="input col-sm-4"><input class="" v-model="item.title" type="text" /></div>
</div>
<div class="row">
<div class="col-12 col-sm-8">
<l-map
:zoom.sync="item.map.zoom"
:center.sync="item.map.center"
style="padding-top: 100%;"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
></l-tile-layer>
<l-control-scale position="topright" :imperial="false" :metric="true"></l-control-scale>
<l-marker v-if="item.hotel && item.hotel.icon" :lat-lng.sync="item.hotel.latlon">
<l-icon>
<div v-html="generate_icon(item.hotel,'hotel')"></div>
</l-icon>
<l-popup>
<div>
Hotel: {{item.hotel.display_name}}
</div>
</l-popup>
<!-- <l-tooltip :options="{ permanent: true, interactive: true }">
<div>
H
</div>
</l-tooltip> -->
</l-marker>
<l-marker v-for="place in item.places.restaurants" :lat-lng.sync="place.latlon">
<l-icon>
<div v-html="generate_icon(place,'food')"></div>
</l-icon>
<l-popup>
<div>
Food: {{place.display_name}}
</div>
</l-popup>
</l-marker>
<l-marker v-for="place in item.places.activities" :lat-lng.sync="place.latlon">
<l-icon>
<div v-html="generate_icon(place,'activity')"></div>
</l-icon>
<l-popup>
<div>
Activity: {{place.display_name}}
</div>
</l-popup>
</l-marker>
</l-map>
</div>
<div class="col-12 col-sm-4">
<div class="row text-center">
<div><label>Date Range</label></div>
<div class="input ">
<date-picker v-model="item.dateRange" range :placeholder="item.dateRange"></date-picker>
</div>
</div>
<div class="row text-center">
<div><label>Hotel</label></div>
<multiselect v-model="item.hotel" id="ajax" label="display_name" track-by="place_id" placeholder="Type to search" open-direction="bottom" :options="query.nominatim" :searchable="true" :loading="querying.hotel" :internal-search="false" :clear-on-select="false" :allow-empty="false" :options-limit="300" :limit="3" :max-height="600" @search-change="debounceSearch.hotel"></multiselect>
</div>
<div class="row text-center">
<div><label>Fight</label></div>
<multiselect v-model="item.flightA" id="ajax" label="label" track-by="id" placeholder="Type to search" open-direction="bottom" :multiple="true" :options="query.flight" :searchable="true" :loading="querying.flight" :internal-search="false" :clear-on-select="false" :allow-empty="false" :options-limit="300" :limit="3" :max-height="600" @search-change="debounceSearch.flight"></multiselect>
<multiselect v-model="item.flightB" id="ajax" label="label" track-by="id" placeholder="Type to search" open-direction="bottom" :multiple="true" :options="query.flight" :searchable="true" :loading="querying.flight" :internal-search="false" :clear-on-select="false" :allow-empty="false" :options-limit="300" :limit="3" :max-height="600" @search-change="debounceSearch.flight"></multiselect>
</div>
<div class="row text-center">
<div><label>Restoration</label></div>
<multiselect v-model="item.places.restaurants" id="ajax" label="display_name" track-by="place_id" placeholder="Type to search" open-direction="bottom" :multiple="true" :options="query.nominatim" :searchable="true" :loading="querying.food" :internal-search="false" :clear-on-select="false" :allow-empty="false" :options-limit="300" :limit="3" :max-height="600" @search-change="debounceSearch.restaurants"></multiselect>
</div>
<div class="row text-center">
<div><label>Activities</label></div>
<multiselect v-model="item.places.activities" id="ajax" label="display_name" track-by="place_id" placeholder="Type to search" open-direction="bottom" :multiple="true" :options="query.nominatim" :searchable="true" :loading="querying.place" :internal-search="false" :clear-on-select="false" :options-limit="300" :limit="3" :max-height="600" @search-change="debounceSearch.places"></multiselect>
</div>
</div>
</div>
</div>
</div>
<div class="">
<div class="container-medium section">
</div>
</div>
<div class="bg-dark">
<div id="go" class="container-medium section text-white">
<h2 class="text-big">Add new section</h2>
<p>Got an other flight and/or hotel ? Add a new section to register it</p>
<div class="aligner aligner--contentEnd">
<button class="button button--primary button--mobileFull" v-on:click="add_section">Add Section</button>
</div>
</div>
</div>
</div>
<div v-else-if="journey_step>=0 && journey_data.main[journey_step_data.section]">
<div class="bg-dark text-white" v-cloak>
<div class="container section">
<div class="row text-center">
<div class="input col-sm-4"><input :value="journey_data.main[journey_step_data.section].title + ': Day ' + journey_step_data.day" disabled /></div>
<div class="input col-sm-2"><input placeholder="Day title" v-model="journey_data.step_title[journey_step]" /></div>
</div>
<div class="row">
<div class="col-12 col-sm-12">
<l-map
:zoom=" journey_data.main[journey_step_data.section].map.zoom"
:center="journey_data.main[journey_step_data.section].map.center"
style="padding-top: 100%;"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
></l-tile-layer>
<l-control-scale position="topright" :imperial="false" :metric="true"></l-control-scale>
<l-marker v-if="journey_data.main[journey_step_data.section].hotel &&
journey_data.main[journey_step_data.section].hotel.icon"
:lat-lng="journey_data.main[journey_step_data.section].hotel.latlon">
<l-icon>
<div v-html="generate_icon(journey_data.main[journey_step_data.section].hotel,'hotel')"></div>
</l-icon>
<l-popup>
<div>
Hotel: {{journey_data.main[journey_step_data.section].hotel.display_name}}
</div>
</l-popup>
</l-marker>
<!-- <l-marker v-for="place in journey_data.main[journey_step_data.section].places.restaurants" :lat-lng="place.latlon">
<l-icon>
<div v-html="generate_icon(place,'food')"></div>
</l-icon>
<l-popup>
<div>
Food: {{place.display_name}}
</div>
</l-popup>
</l-marker> -->
<l-marker v-for="place in filter_selected(journey_data.main[journey_step_data.section].places.activities, true)" :lat-lng="place.latlon">
<l-icon>
<div v-html="generate_icon(place,'activity')"></div>
</l-icon>
<l-popup>
<div>
Activity: {{place.display_name}}
</div>
</l-popup>
</l-marker>
</l-map>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-12">
<div class="container text-center" v-if="journey_step_data.start || journey_step_data.end">
<b>Flights</b>
<div v-if="journey_step_data.start">
<div v-for="element in journey_data.main[journey_step_data.section].flightA"><a :href="'https://www.flightradar24.com/data/flights/'+element.id">{{ element.label }}</a></div>
</div>
<div v-if="journey_step_data.end">
<div v-for="element in journey_data.main[journey_step_data.section].flightB"><a :href="'https://www.flightradar24.com/data/flights/'+element.id">{{ element.label }}</a></div>
</div>
</div>
<div class="container"></div>
<div class="container text-center">
<b>Select Daily Activities</b>
<div class= "row text-dark container">
<div class="col-12 col-sm-6">
<l-draggable
class="dragArea list-group"
:list="filter_unselected(journey_data.main[journey_step_data.section].places.activities)"
:group="{ name: 'people', pull: 'clone', put: false }"
>
<div
class="list-group-item"
v-for="element in filter_unselected(journey_data.main[journey_step_data.section].places.activities)"
:key="element.place_id"
>
{{ element.display_name.split(",")[0] }}
</div>
</l-draggable>
</div>
<div class="col-12 col-sm-6">
<l-draggable
class="dragArea list-group"
:list="activities_tmp"
group="people"
>
<div
class="list-group-item"
v-for="(element,idx) in filter_selected(activities_tmp,true)"
:key="element.place_id"
>
<div class="row">
<i class="fa fa-times close col-12 col-sm-1" @click="remove_item(activities_tmp,idx)"></i>
<div class="text">{{ element.display_name.split(",")[0] }}</div>
</div>
<div class="row">
<div class="input col-12 col-sm-12">
<textarea-autosize
class="small"
placeholder="Comments..."
v-model="element.text"
:min-height="16"
:max-height="360"
/>
</div>
</div>
</div>
</l-draggable>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="">
<div class="container-medium section">
</div>
</div>
<div class="bg-dark">
</div>
</div>
<div class="">
<div class="container-medium section">
<div class="aligner">
<div class="input col-sm-4"><input class="" v-model="impexp" type="text" /></div>
<div class="col-sm-2"><button class="button button--primary button--mobileFull" v-on:click="import_data">Import</button></div>
<div class="col-sm-2"><button class="button button--primary button--mobileFull" v-on:click="export_data">Export</button></div>
</div>
</div>
</div>
</main>
<script src="/public/js/main.js" type="text/javascript" charset="utf-8"></script>
<footer class="bg-dark">
<div class="container">
<div class="section text-center text-small">
<p class="text-white">
<img src="/public/img/helcel.png" alt="helcel logo" width="100"><br><br>
Built with &nbsp;&nbsp; by Helcel <br><span class="text-small text-gray">v0.0.1</span>
</p>
<p class="text-gray"><a href="https://git.helcel.net">Helcel Git</a></p>
</div>
</div>
</footer>
</body>
</html>

1446
template/templates.html Normal file

File diff suppressed because it is too large Load Diff