diff --git a/.gitignore b/.gitignore index 9d3debb..f44e799 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ db/ .yarn/ public/*.js public/*.map +public/*.css .yarnrc.yml +.pnp* +build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c5c8264..c7caa24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ WORKDIR /usr/src/app # where available (npm@5+) COPY package*.json ./ RUN yarn +RUN yarn build # If you are building your code for production # RUN npm ci --only=production # Bundle app source diff --git a/package.json b/package.json index 019efc2..0036cfd 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,14 @@ "name": "volp", "version": "1.0.0", "description": "Open Travel Mapper", - "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "esbuild src/app.ts --bundle --outfile=public/main.js --bundle --minify --sourcemap --tsconfig=tsconfig.json", - "start": "node server.js", - "demon": "nodemon -e ts,pug --watch src --exec \"yarn build && yarn start\"" + "build": "yarn build-server && yarn build-client && yarn build-style", + "build-style": "esbuild src/style/index.css --outfile=public/index.css --bundle --minify ", + "build-client": "esbuild src/client/main.ts --outfile=public/main.js --tree-shaking=true --bundle --minify --sourcemap --tsconfig=tsconfig-client.json", + "build-server": "esbuild src/server/**/*.ts --outdir=build --platform=node --format=cjs", + "start": "node build/main.js", + "demon": "nodemon -e ts,js --watch src --watch template --watch router --exec \"yarn build && yarn start\"" }, "repository": { "type": "git", @@ -19,14 +21,13 @@ "@fastify/leveldb": "^6.0.0", "@fastify/static": "^8.0.0", "@fastify/view": "^10.0.0", - "@prettier/plugin-pug": "^3.0.0", "@types/node": "^22.13.5", - "axios": "^1.7.9", "esbuild": "^0.25.0", - "fastify": "^5.0.0", + "fastify": "^5.2.1", + "jsdom": "^26.0.0", "nodemon": "^3.0.1", - "pretier": "^0.0.1", "prettier": "^3.5.2", - "pug": "^3.0.2" + "pug": "^3.0.2", + "undici": "^7.3.0" } -} +} \ No newline at end of file diff --git a/public/css/index.css b/public/css/index.css deleted file mode 100644 index a04cbc2..0000000 --- a/public/css/index.css +++ /dev/null @@ -1,2928 +0,0 @@ -/** - * SETTINGS - */ -/** - * COMPONENTS - */ -/** - * BACKGROUND - */ -.bg-primary { - background-color: #03a9f4; -} - -.bg-dark { - background-color: #18232f; -} - -.bg-secondary { - background-color: #e91e63; -} - -.bg-white { - background-color: #fff; -} - -.bg-success { - background-color: #4caf50; -} - -.bg-info { - background-color: #5bc0de; -} - -.bg-warning { - background-color: #f0ad4e; -} - -.bg-error { - background-color: #e74c3c; -} - -.bg-gray { - background-color: #969da6; -} - -.bg-gray-light { - background-color: #eceff1; -} - -/** - * BORDER - */ -.border { - border: 1px solid #d5d9db; -} - -.border-bottom { - border-bottom: 1px solid #d5d9db; -} - -.border-left { - border-left: 1px solid #d5d9db; -} - -.border-right { - border-right: 1px solid #d5d9db; -} - -.border-top { - border-top: 1px solid #d5d9db; -} - -/** - * ALIGNERS - */ -.aligner { - display: flex; -} - -.aligner--spaceBetween { - justify-content: space-between; - width: 100%; -} - -.aligner--spaceAround { - justify-content: space-around; - width: 100%; -} - -.aligner--centerVertical { - align-items: center; -} - -.aligner--centerHoritzontal { - justify-content: center; -} - -.aligner--contentStart { - justify-content: flex-start; -} - -.aligner--contentEnd { - justify-content: flex-end; -} - -.aligner--itemTop { - align-self: flex-start; -} - -.aligner--itemBottom { - align-self: flex-end; -} - -.flex-grow, -.aligner--grow { - flex-grow: 1; -} - -/** - * FLOATS - */ -.fleft { - float: left; -} - -.fright { - float: right; -} - -.clearfix ::after { - clear: both; - content: ""; - display: table; -} - -/** - * MARGINS - */ -.m-xsmall { - margin: 4px; -} - -.mb-xsmall { - margin-bottom: 4px; -} - -.m-small { - margin: 8px; -} - -.mb-small { - margin-bottom: 8px; -} - -.m-medium { - margin: 16px; -} - -.mb-medium { - margin-bottom: 16px; -} - -.m-big { - margin: 36px; -} - -.mb-big { - margin-bottom: 36px; -} - -.m-huge { - margin: 48px; -} - -.mb-huge { - margin-bottom: 48px; -} - -.m-none { - margin: 0 !important; -} - -/** - * PADDINGS - */ -.p-small { - padding: 4px; -} - -.pb-small { - padding-bottom: 4px; -} - -.p-medium { - padding: 8px; -} - -.pb-medium { - padding-bottom: 8px; -} - -.p-big { - padding: 16px; -} - -.pb-big { - padding-bottom: 16px; -} - -.p-huge { - padding: 36px; -} - -.pb-huge { - padding-bottom: 36px; -} - -/** - * OTHERS - */ -.no-wrap { - white-space: nowrap; -} - -.overflow-hidden { - overflow: hidden; -} - -.opacity-low { - opacity: 0.5; -} - -.rounded-corners { - border-radius: 5px; -} - -.rounded { - border-radius: 100%; -} - -/** - * LAYOUT - */ -.section { - padding-bottom: 36px; - padding-top: 36px; -} - -@media (min-width: 768px) { - .section { - padding-bottom: 72px; - padding-top: 72px; - } -} - -.section + .section { - padding-top: 0; -} - -.container { - background-color: transparent; - margin: 0 auto; - max-width: 1380px; - padding-left: 12px; - padding-right: 12px; - width: 100%; -} - -@media (min-width: 768px) { - .container { - padding-left: 24px; - padding-right: 24px; - } -} - -.container-medium { - margin: 0 auto; - max-width: 944px; - padding-left: 12px; - padding-right: 12px; -} - -@media (min-width: 768px) { - .container-medium { - padding-left: 24px; - padding-right: 24px; - } -} - -.container-small { - margin: 0 auto; - max-width: 400px; - padding-left: 12px; - padding-right: 12px; -} - -@media (min-width: 768px) { - .container-small { - padding-left: 24px; - padding-right: 24px; - } -} - -/** - * TYPOGRAPHY - */ -body { - color: #272727; - font-family: "Lato", sans-serif; - font-size: 16px; - font-weight: 400; - line-height: 1.5em; -} - -a { - color: #03a9f4; - text-decoration: none; -} - -a:hover { - color: rgba(3, 169, 244, 0.8); -} - -a:focus { - color: #03a9f4; -} - -.text-huge, -.text-big, -.text-medium { - margin-bottom: 1em; -} - -.text-huge { - font-size: 36px; - line-height: 1.3em; -} - -.text-big { - font-size: 24px; - line-height: 1.3em; -} - -.text-medium { - font-size: 16px; - line-height: 1.5em; -} - -.text-small { - font-size: 12px; - line-height: 1.3em; -} - -.text-body { - font-size: 16px; - line-height: 1.5em; -} - -.text-primary { - color: #03a9f4; -} - -.text-dark { - color: #18232f; -} - -.text-secondary { - color: #e91e63; -} - -.text-white { - color: #fff; -} - -.text-success { - color: #4caf50; -} - -.text-info { - color: #5bc0de; -} - -.text-warning { - color: #f0ad4e; -} - -.text-error { - color: #e74c3c; -} - -.text-gray { - color: #969da6; -} - -.text-gray-light { - color: #eceff1; -} - -.text-light { - font-weight: 300; -} - -.text-normal { - font-weight: 400; -} - -.text-lineThrough { - text-decoration: line-through; -} - -.text-italic { - font-style: italic; -} - -.text-underline { - text-decoration: underline; -} - -.text-uppercase { - text-transform: uppercase; -} - -.text-withSubtitle { - margin-bottom: 0 !important; -} - -.text-withSubtitle + .text-huge, -.text-withSubtitle + .text-big, -.text-withSubtitle + .text-medium, -.text-withSubtitle + .text-small { - margin-top: 0.5em; -} - -h1, -h2, -h3, -h4 { - font-weight: 300; -} - -.text-center { - text-align: center; -} - -.text-right { - text-align: right; -} - -.text-left { - text-align: left; -} - -/** - * BADGE - * - * Markup: - * ------- - * - * - * - */ -.badge { - background-color: #eceff1; - border-radius: 3px; - color: #272727; - display: inline-block; - line-height: 1.2em; - padding: 8px 16px; -} - -.badge--big { - font-size: 1.3em; -} - -.badge--small { - font-size: 0.7em; -} - -.badge--primary { - background-color: #03a9f4; - color: #fff; -} - -.badge--secondary { - background-color: #e91e63; - color: #fff; -} - -.badge--dark { - background-color: #18232f; - color: #fff; -} - -.badge--light { - background-color: #969da6; - color: #fff; -} - -.badge--success { - background-color: #4caf50; - color: #fff; -} - -.badge--error { - background-color: #e74c3c; - color: #fff; -} - -.badge--warning { - background-color: #f0ad4e; - color: #fff; -} - -/** - * BUTTON - * - * Markup: - * ------- - * - * - * - * - * - */ -button { - background-color: transparent; - cursor: pointer; -} - -.button { - background-color: #03a9f4; - border: 0; - border-radius: 200px; - color: #fff; - display: inline-block; - font-family: "Lato", sans-serif; - font-size: 16px; - font-weight: 600; - line-height: 1.5em; - margin: 0 8px 16px 8px; - padding: 8px 16px; - text-align: center; - text-decoration: none; - text-transform: uppercase; - transition: opacity 0.2s ease-in-out; - white-space: nowrap; -} - -.button:focus, -.button:hover, -.button:active { - color: #fff; -} - -.button:hover { - background-color: rgba(3, 169, 244, 0.8); - color: #fff; - cursor: pointer; - text-decoration: none; -} - -.button:active { - opacity: 1; -} - -.button:first-child { - margin-left: 0; -} - -.button:last-child { - margin-right: 0; -} - -.button--big { - font-size: 24px; - padding: 16px 36px; -} - -.button--small { - font-size: 12px; - padding: 4px 16px; -} - -@media (max-width: 991px) { - .button--mobileFull { - margin-left: 0; - margin-right: 0; - width: 100%; - } -} - -.button--secondary { - background-color: #e91e63; - color: #fff; -} - -.button--secondary:hover { - background-color: rgba(233, 30, 99, 0.8); - color: #fff; -} - -.button--white { - background-color: #fff; - color: #03a9f4; -} - -.button--white:hover { - background-color: rgba(255, 255, 255, 0.8); - color: rgba(3, 169, 244, 0.8); -} - -.button--green { - background-color: #4caf50; - color: #fff; -} - -.button--green:hover { - background-color: rgba(76, 175, 80, 0.8); - color: #fff; -} - -.button--red { - background-color: #e74c3c; - color: #fff; -} - -.button--red:hover { - background-color: rgba(231, 76, 60, 0.8); - color: #fff; -} - -.button--transparent { - background-color: transparent; - color: #03a9f4; -} - -.button--transparent:active, -.button--transparent:hover, -.button--transparent:focus { - background-color: transparent; - color: rgba(3, 169, 244, 0.8); - opacity: 0.8; -} - -.button--outlined { - background-color: transparent; - border: 1px solid #d5d9db; - color: #03a9f4; -} - -.button--outlined:active, -.button--outlined:hover, -.button--outlined:focus { - background-color: transparent; - color: rgba(3, 169, 244, 0.8); - opacity: 0.8; -} - -/** - * FORMS - * - * Markup: - * --------- - *
- * - *
- * - *
- * - *
- * - *
- * - *
- * - *
- * - * - *
- * - *
- * - * - *
- */ -input, -textarea { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -label { - display: block; - font-weight: normal; -} - -input:-webkit-autofill { - box-shadow: 0 0 0 1000px #eceff1 inset; -} - -.textarea, -.input, -.select { - border: 1px solid #d5d9db; - border-radius: 3px; - box-shadow: none; - display: inline-block; - font-weight: normal; - margin-bottom: 20px; - overflow: hidden; -} - -.textarea :focus, -.input :focus, -.select :focus { - outline: none; -} - -.textarea.has-error, -.input.has-error, -.select.has-error { - background: #eceff1; - border: 1px solid #e74c3c; - margin-bottom: 0; -} - -.select { - background-color: #eceff1; - display: inline-block; - margin-right: 16px; - position: relative; -} - -.select:last-child { - margin-right: 0; -} - -.select-fullWidth { - display: block; - margin-left: 0; - margin-right: 0; - width: 100%; -} - -.select select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: transparent; - border: 0; - border-radius: 0; - color: #272727; - display: block; - font-size: 16px; - line-height: 1.5em; - margin: 0; - padding: 8px 16px; - padding-right: 30px; - transition: background-color 0.2s ease-in-out; - width: 100%; -} - -.select select:active, -.select select:focus { - background-color: #fbfbfc; - border: 0; - outline: none; -} - -.select select::-ms-expand { - display: none; -} - -.select::after, -.select::before { - background: #03a9f4; - content: ""; - display: block; - height: 2px; - margin-top: 2px; - position: absolute; - right: 5px; - top: 50%; - -webkit-transform-origin: 1px; - transform-origin: 1px; - width: 10px; -} - -.select::after { - -webkit-transform: rotate(-135deg); - transform: rotate(-135deg); -} - -.select::before { - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); -} - -.textarea { - background-color: #eceff1; - padding: 0; -} - -.textarea-fullWidth { - display: block; - margin-left: 0; - margin-right: 0; - width: 100%; -} - -.textarea textarea { - background: transparent; - border: 0; - color: #272727; - display: block; - font-family: "Lato", sans-serif; - font-size: 16px; - line-height: 1.5em; - margin: 0; - min-height: 120px; - padding: 8px 16px; - transition: background-color 0.2s ease-in-out; - width: 100%; -} - -.textarea textarea::-webkit-input-placeholder { - color: #969da6; -} - -.textarea textarea::-ms-input-placeholder { - color: #969da6; -} - -.textarea textarea::placeholder { - color: #969da6; -} - -.textarea textarea:focus, -.textarea textarea:active { - background-color: #fbfbfc; - border: 0; - outline: none; -} - -.checkbox { - margin-bottom: 8px; - position: relative; -} - -.checkbox input[type="checkbox"] { - display: none; -} - -.checkbox input[type="checkbox"]:checked + label::after { - -webkit-animation: checkboxAndRadioAnimation 0.25s; - animation: checkboxAndRadioAnimation 0.25s; - content: ""; - -webkit-transform: scale(1) rotate(45deg); - transform: scale(1) rotate(45deg); -} - -.checkbox input[type="checkbox"] + label { - display: block; - overflow: hidden; - padding-left: 30px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.checkbox input[type="checkbox"] + label::before { - background-color: #eceff1; - border: 1px solid #d5d9db; - border-radius: 3px; - content: ""; - display: inline-block; - height: 20px; - left: 0; - margin-top: -10px; - position: absolute; - top: 50%; - width: 20px; -} - -.checkbox input[type="checkbox"] + label::after { - border-bottom: 3px solid #03a9f4; - border-right: 3px solid #03a9f4; - display: block; - height: 12px; - left: 11px; - margin-left: -4px; - margin-top: -7px; - position: absolute; - top: 50%; - width: 7px; - z-index: 1; -} - -.radio { - margin-bottom: 8px; - position: relative; -} - -.radio input[type="radio"] { - display: none; -} - -.radio input[type="radio"]:checked + label::after { - -webkit-animation: checkboxAndRadioAnimation 0.25s; - animation: checkboxAndRadioAnimation 0.25s; - content: ""; - -webkit-transform: scale(1) rotate(45deg); - transform: scale(1) rotate(45deg); -} - -.radio input[type="radio"] + label { - display: block; - overflow: hidden; - padding-left: 30px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.radio input[type="radio"] + label::before { - background-color: #eceff1; - border: 1px solid #d5d9db; - border-radius: 20px; - content: ""; - display: inline-block; - height: 20px; - left: 0; - margin-top: -10px; - position: absolute; - top: 50%; - width: 20px; -} - -.radio input[type="radio"] + label::after { - background-color: #03a9f4; - border-radius: 20px; - display: block; - height: 10px; - left: 11px; - margin-left: -6px; - margin-top: -6px; - position: absolute; - top: 13px; - width: 10px; - z-index: 1; -} - -@-webkit-keyframes checkboxAndRadioAnimation { - 0% { - -webkit-transform: scale(0) rotate(45deg); - transform: scale(0) rotate(45deg); - } - 50% { - -webkit-transform: scale(1.5) rotate(45deg); - transform: scale(1.5) rotate(45deg); - } - 100% { - -webkit-transform: scale(1) rotate(45deg); - transform: scale(1) rotate(45deg); - } -} - -@keyframes checkboxAndRadioAnimation { - 0% { - -webkit-transform: scale(0) rotate(45deg); - transform: scale(0) rotate(45deg); - } - 50% { - -webkit-transform: scale(1.5) rotate(45deg); - transform: scale(1.5) rotate(45deg); - } - 100% { - -webkit-transform: scale(1) rotate(45deg); - transform: scale(1) rotate(45deg); - } -} - -.input-invis { - background-color: transparent !important; - margin: auto !important; - border: 0 !important; -} - -.input { - background-color: #eceff1; - margin-right: 10px; - padding: 0; - position: relative; -} - -.input :focus, -.input :active { - background-color: #fbfbfc; - border-radius: 3px; -} - -.input input, -.input textarea { - background: transparent; - border: 0; - box-shadow: none; - color: #272727; - font-size: 16px; - line-height: 1.5em; - margin: 0; - outline: none; - padding: 8px 16px; - width: 100%; -} - -.input input::-webkit-input-placeholder { - color: #969da6; -} - -.input input::-ms-input-placeholder { - color: #969da6; -} - -.input input::placeholder { - color: #969da6; -} - -.input input.small { - line-height: 1em; - padding: 0; -} - -.input-withIcon input { - padding-right: 32px; -} - -.input-icon { - fill: #969da6; - height: 16px; - margin-top: -8px; - position: absolute; - right: 16px; - top: 50%; - width: 16px; -} - -.input-fullWidth { - display: block; - margin-left: 0; - margin-right: 0; - width: 100%; -} - -/** - * FORM COLLAPSED (items in row without gap between them) - * - * Markup: - * ------- - * - *
- *
- * - *
- *
- * - *
- * - *
- **/ -.formCollapsed { - display: flex; - margin-bottom: 20px; -} - -.formCollapsed-item { - border-radius: 0 !important; - margin: 0 !important; -} - -.formCollapsed-item:first-child { - border-bottom-left-radius: 3px !important; - border-top-left-radius: 3px !important; -} - -.formCollapsed-item:last-child { - border-bottom-right-radius: 3px !important; - border-top-right-radius: 3px !important; -} - -.formCollapsed-item:not(:last-child) { - border-right: 0; -} - -.formCollapsed-itemPrimary { - flex: 1; -} - -/** - * LOADING BAR - * - * Markup: - * --------- - *
- * - */ -.loadingBar { - height: 6px; - left: 0; - overflow: hidden; - position: fixed; - right: 0; - top: 0; - width: 100%; - z-index: 1000; -} - -.loadingBar::before { - -webkit-animation: loading 2s linear infinite; - animation: loading 2s linear infinite; - background-color: #03a9f4; - content: ""; - display: block; - height: 6px; - left: -300px; - position: absolute; - width: 300px; -} - -@-webkit-keyframes loading { - from { - left: -300px; - width: 30%; - } - 50% { - width: 30%; - } - 70% { - width: 70%; - } - 80% { - left: 50%; - } - 95% { - left: 120%; - } - to { - left: 100%; - } -} - -@keyframes loading { - from { - left: -300px; - width: 30%; - } - 50% { - width: 30%; - } - 70% { - width: 70%; - } - 80% { - left: 50%; - } - 95% { - left: 120%; - } - to { - left: 100%; - } -} - -/** - * LOADING SPINNER - * - * Markup: - * --------- - *
- * - * - * - * - *
- * - */ -.loadingSpinner { - -webkit-animation: rotateLoader 4s infinite; - animation: rotateLoader 4s infinite; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - display: block; - height: 30px; - left: 50%; - margin-left: -15px; - margin-top: -15px; - position: fixed; - top: 50%; - width: 30px; - z-index: 1000; -} - -.loadingSpinner-inner { - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - background-color: #e91e63; - border-radius: 100%; - display: block; - height: 9px; - position: absolute; - width: 9px; -} - -.loadingSpinner-inner:nth-child(1) { - -webkit-animation: translateBall1 1s infinite; - animation: translateBall1 1s infinite; - left: 0; - top: 0; - -webkit-transform: translate3d(4.5px, 4.5px, 0); - transform: translate3d(4.5px, 4.5px, 0); -} - -.loadingSpinner-inner:nth-child(2) { - -webkit-animation: translateBall2 1s infinite; - animation: translateBall2 1s infinite; - right: 0; - top: 0; -} - -.loadingSpinner-inner:nth-child(3) { - -webkit-animation: translateBall3 1s infinite; - animation: translateBall3 1s infinite; - bottom: 0; - right: 0; -} - -.loadingSpinner-inner:nth-child(4) { - -webkit-animation: translateBall4 1s infinite; - animation: translateBall4 1s infinite; - bottom: 0; - left: 0; -} - -@-webkit-keyframes rotateLoader { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); - } - 25% { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); - } - 50% { - -webkit-transform: rotate(180deg); - transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} - -@keyframes rotateLoader { - 0% { - -webkit-transform: rotate(0); - transform: rotate(0); - } - 25% { - -webkit-transform: rotate(90deg); - transform: rotate(90deg); - } - 50% { - -webkit-transform: rotate(180deg); - transform: rotate(180deg); - } - 75% { - -webkit-transform: rotate(270deg); - transform: rotate(270deg); - } - 100% { - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - } -} - -@-webkit-keyframes translateBall1 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(4.5px, 4.5px, 0); - transform: translate3d(4.5px, 4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes translateBall1 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(4.5px, 4.5px, 0); - transform: translate3d(4.5px, 4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@-webkit-keyframes translateBall2 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(-4.5px, 4.5px, 0); - transform: translate3d(-4.5px, 4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes translateBall2 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(-4.5px, 4.5px, 0); - transform: translate3d(-4.5px, 4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@-webkit-keyframes translateBall3 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(-4.5px, -4.5px, 0); - transform: translate3d(-4.5px, -4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes translateBall3 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(-4.5px, -4.5px, 0); - transform: translate3d(-4.5px, -4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@-webkit-keyframes translateBall4 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(4.5px, -4.5px, 0); - transform: translate3d(4.5px, -4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes translateBall4 { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 50% { - -webkit-transform: translate3d(4.5px, -4.5px, 0); - transform: translate3d(4.5px, -4.5px, 0); - } - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -/** - * NOTIFICATION - * - * Markup: - * ------- - * - *
Success notification
- *
Success info
- *
Success error
- *
Success warning
- * - */ -.notification { - border-radius: 3px; - color: #fff; - margin-bottom: 36px; - padding: 16px; -} - -.notification p:last-child { - margin-bottom: 0; -} - -.notification--primary { - background-color: #03a9f4; -} - -.notification--dark { - background-color: #18232f; -} - -.notification--secondary { - background-color: #e91e63; -} - -.notification--white { - background-color: #fff; -} - -.notification--success { - background-color: #4caf50; -} - -.notification--info { - background-color: #5bc0de; -} - -.notification--warning { - background-color: #f0ad4e; -} - -.notification--error { - background-color: #e74c3c; -} - -.notification--gray { - background-color: #969da6; -} - -.notification--gray-light { - background-color: #eceff1; -} - -/** - * PAGINATOR - * - * Markup: - * ------- - * - * - */ -.paginator-item { - display: inline-block; - margin-right: 4px; -} - -.paginator-itemLink { - background-color: #eceff1; - border-radius: 3px; - display: block; - padding: 8px 16px; -} - -.paginator-itemLink.is-active { - background-color: #03a9f4; - color: #fff; - cursor: default; -} - -/** - * TABLE - * - * Markup: - * ------- - * - * - * - * - * - * - * - * - * Blue - * One - * My life fades - * - *
First columnSecond columnThird column
- * - */ -.table { - background-color: #eceff1; - border: 1px solid #d5d9db; - border-collapse: collapse; - color: #272727; - max-width: 100%; - width: 100%; -} - -.table th, -.table td { - border-bottom: 1px solid #d5d9db; - padding: 8px; - position: relative; -} - -.table thead { - border-bottom: 1px solid #d5d9db; -} - -.table th { - background-color: #fff; - color: #969da6; - font-size: 12px; - font-weight: normal; - padding: 8px; - white-space: nowrap; -} - -/** - * TABLE RESPONSIVE - * - * Markup: - * ------- - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
First columnSecond columnThird column
BlueOneMy life fades
GreenTwo - * when the world was powered by the black fuel... and the desert - * sprouted great cities of pipe and steel. - *
YellowThree - * A whirlwind of looting, a firestorm of fear. - *
- * - */ -.table--responsive th { - display: none; -} - -.table--responsive td { - display: block; -} - -@media (max-width: 767px) { - .table--responsive td { - border: 0; - } -} - -.table--responsive td::before { - color: #969da6; - content: attr(data-th) ": "; - display: block; - font-weight: normal; -} - -@media (min-width: 576px) { - .table--responsive td::before { - display: none; - } -} - -.table--responsive td:first-child { - border-top: 1px solid #d5d9db; -} - -.table--responsive th, -.table--responsive td { - text-align: left; -} - -@media (min-width: 576px) { - .table--responsive th, - .table--responsive td { - border-top: 1px solid #d5d9db; - display: table-cell; - } -} - -/** - * TABS - * - * Markup: - * ------- - * - *
- * [...] - * [...] - *
- * - */ -.tabs { - border-bottom: 1px solid #d5d9db; - text-align: center; -} - -.tabs-item { - border-bottom: 3px solid transparent; - color: #969da6; - display: inline-block; - margin: 0 16px 0 0; - min-width: 70px; - padding: 16px; - position: relative; -} - -.tabs-item:hover { - color: #03a9f4; - text-decoration: none; -} - -.tabs-item.is-selected { - border-bottom: 3px solid #03a9f4; - color: #03a9f4; -} - -/** - * TAG - * - * Markup: - * ------- - * - * - * - * - * - */ -.tag { - background-color: #eceff1; - border-radius: 3px 0 0 3px; - color: #272727; - display: inline-block; - line-height: 16px; - margin: 0 16px 16px 0; - padding: 8px; - position: relative; -} - -.tag::before { - border-bottom: 16px solid transparent; - border-left: 8px solid #eceff1; - border-top: 16px solid transparent; - content: ""; - height: 0; - position: absolute; - right: -8px; - top: 0; - width: 0; -} - -.tag::after { - background: #fff; - border-radius: 100%; - content: ""; - height: 5px; - margin-top: -2.5px; - position: absolute; - right: -2.5px; - top: 50%; - width: 5px; -} - -.container { - width: 100%; - padding-right: 12px; - padding-left: 12px; - margin-right: auto; - margin-left: auto; -} - -@media (min-width: 576px) { - .container { - max-width: 540px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 720px; - } -} - -@media (min-width: 992px) { - .container { - max-width: 960px; - } -} - -@media (min-width: 1200px) { - .container { - max-width: 1140px; - } -} - -.container-fluid { - width: 100%; - padding-right: 12px; - padding-left: 12px; - margin-right: auto; - margin-left: auto; -} - -.row { - display: flex; - flex-wrap: wrap; - margin-right: -12px; - margin-left: -12px; -} - -.no-gutters { - margin-left: 0; - margin-right: 0; -} - -.no-gutters > .col, -.no-gutters > [class*="col-"] { - padding-left: 0; - padding-right: 0; -} - -.col-1, -.col-2, -.col-3, -.col-4, -.col-5, -.col-6, -.col-7, -.col-8, -.col-9, -.col-10, -.col-11, -.col-12, -.col, -.col-auto, -.col-sm-1, -.col-sm-2, -.col-sm-3, -.col-sm-4, -.col-sm-5, -.col-sm-6, -.col-sm-7, -.col-sm-8, -.col-sm-9, -.col-sm-10, -.col-sm-11, -.col-sm-12, -.col-sm, -.col-sm-auto, -.col-md-1, -.col-md-2, -.col-md-3, -.col-md-4, -.col-md-5, -.col-md-6, -.col-md-7, -.col-md-8, -.col-md-9, -.col-md-10, -.col-md-11, -.col-md-12, -.col-md, -.col-md-auto, -.col-lg-1, -.col-lg-2, -.col-lg-3, -.col-lg-4, -.col-lg-5, -.col-lg-6, -.col-lg-7, -.col-lg-8, -.col-lg-9, -.col-lg-10, -.col-lg-11, -.col-lg-12, -.col-lg, -.col-lg-auto, -.col-xl-1, -.col-xl-2, -.col-xl-3, -.col-xl-4, -.col-xl-5, -.col-xl-6, -.col-xl-7, -.col-xl-8, -.col-xl-9, -.col-xl-10, -.col-xl-11, -.col-xl-12, -.col-xl, -.col-xl-auto { - position: relative; - width: 100%; - min-height: 1px; - padding-right: 12px; - padding-left: 12px; -} - -.col { - flex-basis: 0; - flex-grow: 1; - max-width: 100%; -} - -.col-auto { - flex: 0 0 auto; - width: auto; - max-width: none; -} - -.col-1 { - flex: 0 0 8.33333%; - max-width: 8.33333%; -} - -.col-2 { - flex: 0 0 16.66667%; - max-width: 16.66667%; -} - -.col-3 { - flex: 0 0 25%; - max-width: 25%; -} - -.col-4 { - flex: 0 0 33.33333%; - max-width: 33.33333%; -} - -.col-5 { - flex: 0 0 41.66667%; - max-width: 41.66667%; -} - -.col-6 { - flex: 0 0 50%; - max-width: 50%; -} - -.col-7 { - flex: 0 0 58.33333%; - max-width: 58.33333%; -} - -.col-8 { - flex: 0 0 66.66667%; - max-width: 66.66667%; -} - -.col-9 { - flex: 0 0 75%; - max-width: 75%; -} - -.col-10 { - flex: 0 0 83.33333%; - max-width: 83.33333%; -} - -.col-11 { - flex: 0 0 91.66667%; - max-width: 91.66667%; -} - -.col-12 { - flex: 0 0 100%; - max-width: 100%; -} - -.order-first { - order: -1; -} - -.order-1 { - order: 1; -} - -.order-2 { - order: 2; -} - -.order-3 { - order: 3; -} - -.order-4 { - order: 4; -} - -.order-5 { - order: 5; -} - -.order-6 { - order: 6; -} - -.order-7 { - order: 7; -} - -.order-8 { - order: 8; -} - -.order-9 { - order: 9; -} - -.order-10 { - order: 10; -} - -.order-11 { - order: 11; -} - -.order-12 { - order: 12; -} - -.offset-1 { - margin-left: 8.33333%; -} - -.offset-2 { - margin-left: 16.66667%; -} - -.offset-3 { - margin-left: 25%; -} - -.offset-4 { - margin-left: 33.33333%; -} - -.offset-5 { - margin-left: 41.66667%; -} - -.offset-6 { - margin-left: 50%; -} - -.offset-7 { - margin-left: 58.33333%; -} - -.offset-8 { - margin-left: 66.66667%; -} - -.offset-9 { - margin-left: 75%; -} - -.offset-10 { - margin-left: 83.33333%; -} - -.offset-11 { - margin-left: 91.66667%; -} - -@media (min-width: 576px) { - .col-sm { - flex-basis: 0; - flex-grow: 1; - max-width: 100%; - } - .col-sm-auto { - flex: 0 0 auto; - width: auto; - max-width: none; - } - .col-sm-1 { - flex: 0 0 8.33333%; - max-width: 8.33333%; - } - .col-sm-2 { - flex: 0 0 16.66667%; - max-width: 16.66667%; - } - .col-sm-3 { - flex: 0 0 25%; - max-width: 25%; - } - .col-sm-4 { - flex: 0 0 33.33333%; - max-width: 33.33333%; - } - .col-sm-5 { - flex: 0 0 41.66667%; - max-width: 41.66667%; - } - .col-sm-6 { - flex: 0 0 50%; - max-width: 50%; - } - .col-sm-7 { - flex: 0 0 58.33333%; - max-width: 58.33333%; - } - .col-sm-8 { - flex: 0 0 66.66667%; - max-width: 66.66667%; - } - .col-sm-9 { - flex: 0 0 75%; - max-width: 75%; - } - .col-sm-10 { - flex: 0 0 83.33333%; - max-width: 83.33333%; - } - .col-sm-11 { - flex: 0 0 91.66667%; - max-width: 91.66667%; - } - .col-sm-12 { - flex: 0 0 100%; - max-width: 100%; - } - .order-sm-first { - order: -1; - } - .order-sm-1 { - order: 1; - } - .order-sm-2 { - order: 2; - } - .order-sm-3 { - order: 3; - } - .order-sm-4 { - order: 4; - } - .order-sm-5 { - order: 5; - } - .order-sm-6 { - order: 6; - } - .order-sm-7 { - order: 7; - } - .order-sm-8 { - order: 8; - } - .order-sm-9 { - order: 9; - } - .order-sm-10 { - order: 10; - } - .order-sm-11 { - order: 11; - } - .order-sm-12 { - order: 12; - } - .offset-sm-0 { - margin-left: 0; - } - .offset-sm-1 { - margin-left: 8.33333%; - } - .offset-sm-2 { - margin-left: 16.66667%; - } - .offset-sm-3 { - margin-left: 25%; - } - .offset-sm-4 { - margin-left: 33.33333%; - } - .offset-sm-5 { - margin-left: 41.66667%; - } - .offset-sm-6 { - margin-left: 50%; - } - .offset-sm-7 { - margin-left: 58.33333%; - } - .offset-sm-8 { - margin-left: 66.66667%; - } - .offset-sm-9 { - margin-left: 75%; - } - .offset-sm-10 { - margin-left: 83.33333%; - } - .offset-sm-11 { - margin-left: 91.66667%; - } -} - -@media (min-width: 768px) { - .col-md { - flex-basis: 0; - flex-grow: 1; - max-width: 100%; - } - .col-md-auto { - flex: 0 0 auto; - width: auto; - max-width: none; - } - .col-md-1 { - flex: 0 0 8.33333%; - max-width: 8.33333%; - } - .col-md-2 { - flex: 0 0 16.66667%; - max-width: 16.66667%; - } - .col-md-3 { - flex: 0 0 25%; - max-width: 25%; - } - .col-md-4 { - flex: 0 0 33.33333%; - max-width: 33.33333%; - } - .col-md-5 { - flex: 0 0 41.66667%; - max-width: 41.66667%; - } - .col-md-6 { - flex: 0 0 50%; - max-width: 50%; - } - .col-md-7 { - flex: 0 0 58.33333%; - max-width: 58.33333%; - } - .col-md-8 { - flex: 0 0 66.66667%; - max-width: 66.66667%; - } - .col-md-9 { - flex: 0 0 75%; - max-width: 75%; - } - .col-md-10 { - flex: 0 0 83.33333%; - max-width: 83.33333%; - } - .col-md-11 { - flex: 0 0 91.66667%; - max-width: 91.66667%; - } - .col-md-12 { - flex: 0 0 100%; - max-width: 100%; - } - .order-md-first { - order: -1; - } - .order-md-1 { - order: 1; - } - .order-md-2 { - order: 2; - } - .order-md-3 { - order: 3; - } - .order-md-4 { - order: 4; - } - .order-md-5 { - order: 5; - } - .order-md-6 { - order: 6; - } - .order-md-7 { - order: 7; - } - .order-md-8 { - order: 8; - } - .order-md-9 { - order: 9; - } - .order-md-10 { - order: 10; - } - .order-md-11 { - order: 11; - } - .order-md-12 { - order: 12; - } - .offset-md-0 { - margin-left: 0; - } - .offset-md-1 { - margin-left: 8.33333%; - } - .offset-md-2 { - margin-left: 16.66667%; - } - .offset-md-3 { - margin-left: 25%; - } - .offset-md-4 { - margin-left: 33.33333%; - } - .offset-md-5 { - margin-left: 41.66667%; - } - .offset-md-6 { - margin-left: 50%; - } - .offset-md-7 { - margin-left: 58.33333%; - } - .offset-md-8 { - margin-left: 66.66667%; - } - .offset-md-9 { - margin-left: 75%; - } - .offset-md-10 { - margin-left: 83.33333%; - } - .offset-md-11 { - margin-left: 91.66667%; - } -} - -@media (min-width: 992px) { - .col-lg { - flex-basis: 0; - flex-grow: 1; - max-width: 100%; - } - .col-lg-auto { - flex: 0 0 auto; - width: auto; - max-width: none; - } - .col-lg-1 { - flex: 0 0 8.33333%; - max-width: 8.33333%; - } - .col-lg-2 { - flex: 0 0 16.66667%; - max-width: 16.66667%; - } - .col-lg-3 { - flex: 0 0 25%; - max-width: 25%; - } - .col-lg-4 { - flex: 0 0 33.33333%; - max-width: 33.33333%; - } - .col-lg-5 { - flex: 0 0 41.66667%; - max-width: 41.66667%; - } - .col-lg-6 { - flex: 0 0 50%; - max-width: 50%; - } - .col-lg-7 { - flex: 0 0 58.33333%; - max-width: 58.33333%; - } - .col-lg-8 { - flex: 0 0 66.66667%; - max-width: 66.66667%; - } - .col-lg-9 { - flex: 0 0 75%; - max-width: 75%; - } - .col-lg-10 { - flex: 0 0 83.33333%; - max-width: 83.33333%; - } - .col-lg-11 { - flex: 0 0 91.66667%; - max-width: 91.66667%; - } - .col-lg-12 { - flex: 0 0 100%; - max-width: 100%; - } - .order-lg-first { - order: -1; - } - .order-lg-1 { - order: 1; - } - .order-lg-2 { - order: 2; - } - .order-lg-3 { - order: 3; - } - .order-lg-4 { - order: 4; - } - .order-lg-5 { - order: 5; - } - .order-lg-6 { - order: 6; - } - .order-lg-7 { - order: 7; - } - .order-lg-8 { - order: 8; - } - .order-lg-9 { - order: 9; - } - .order-lg-10 { - order: 10; - } - .order-lg-11 { - order: 11; - } - .order-lg-12 { - order: 12; - } - .offset-lg-0 { - margin-left: 0; - } - .offset-lg-1 { - margin-left: 8.33333%; - } - .offset-lg-2 { - margin-left: 16.66667%; - } - .offset-lg-3 { - margin-left: 25%; - } - .offset-lg-4 { - margin-left: 33.33333%; - } - .offset-lg-5 { - margin-left: 41.66667%; - } - .offset-lg-6 { - margin-left: 50%; - } - .offset-lg-7 { - margin-left: 58.33333%; - } - .offset-lg-8 { - margin-left: 66.66667%; - } - .offset-lg-9 { - margin-left: 75%; - } - .offset-lg-10 { - margin-left: 83.33333%; - } - .offset-lg-11 { - margin-left: 91.66667%; - } -} - -@media (min-width: 1200px) { - .col-xl { - flex-basis: 0; - flex-grow: 1; - max-width: 100%; - } - .col-xl-auto { - flex: 0 0 auto; - width: auto; - max-width: none; - } - .col-xl-1 { - flex: 0 0 8.33333%; - max-width: 8.33333%; - } - .col-xl-2 { - flex: 0 0 16.66667%; - max-width: 16.66667%; - } - .col-xl-3 { - flex: 0 0 25%; - max-width: 25%; - } - .col-xl-4 { - flex: 0 0 33.33333%; - max-width: 33.33333%; - } - .col-xl-5 { - flex: 0 0 41.66667%; - max-width: 41.66667%; - } - .col-xl-6 { - flex: 0 0 50%; - max-width: 50%; - } - .col-xl-7 { - flex: 0 0 58.33333%; - max-width: 58.33333%; - } - .col-xl-8 { - flex: 0 0 66.66667%; - max-width: 66.66667%; - } - .col-xl-9 { - flex: 0 0 75%; - max-width: 75%; - } - .col-xl-10 { - flex: 0 0 83.33333%; - max-width: 83.33333%; - } - .col-xl-11 { - flex: 0 0 91.66667%; - max-width: 91.66667%; - } - .col-xl-12 { - flex: 0 0 100%; - max-width: 100%; - } - .order-xl-first { - order: -1; - } - .order-xl-1 { - order: 1; - } - .order-xl-2 { - order: 2; - } - .order-xl-3 { - order: 3; - } - .order-xl-4 { - order: 4; - } - .order-xl-5 { - order: 5; - } - .order-xl-6 { - order: 6; - } - .order-xl-7 { - order: 7; - } - .order-xl-8 { - order: 8; - } - .order-xl-9 { - order: 9; - } - .order-xl-10 { - order: 10; - } - .order-xl-11 { - order: 11; - } - .order-xl-12 { - order: 12; - } - .offset-xl-0 { - margin-left: 0; - } - .offset-xl-1 { - margin-left: 8.33333%; - } - .offset-xl-2 { - margin-left: 16.66667%; - } - .offset-xl-3 { - margin-left: 25%; - } - .offset-xl-4 { - margin-left: 33.33333%; - } - .offset-xl-5 { - margin-left: 41.66667%; - } - .offset-xl-6 { - margin-left: 50%; - } - .offset-xl-7 { - margin-left: 58.33333%; - } - .offset-xl-8 { - margin-left: 66.66667%; - } - .offset-xl-9 { - margin-left: 75%; - } - .offset-xl-10 { - margin-left: 83.33333%; - } - .offset-xl-11 { - margin-left: 91.66667%; - } -} - -/** - * Demo website styles - */ -body { - padding-top: 60px; -} - -@media (min-width: 576px) { - body { - padding-top: 80px; - } -} - -.anchor { - display: block; - position: relative; - top: -65px; - visibility: hidden; -} - -.main-logo { - width: 150px; - fill: #03a9f4; -} - -@media (max-width: 767px) { - .main-logo { - width: 100px; - } -} - -@media screen and (max-width: 550px) { - .example-image { - width: 50px; - } -} - -.examples { - width: 100%; -} - -.example1, -.example2 { - background-color: #eceff1; - display: inline-block; - max-width: 300px; - padding: 16px; - text-align: center; -} - -@media screen and (max-width: 550px) { - .example1, - .example2 { - max-width: 100%; - } - .example1 img, - .example2 img { - width: 100% !important; - } -} - -.language-js { - font-size: 14px; -} - -:not(pre) > code[class*="language-"], -pre[class*="language-"] { - background-color: #eceff1 !important; -} - -pre { - margin-bottom: 2em !important; - overflow-x: auto; -} - -.example-code { - max-width: 700px; -} - -.example-code pre[class*="language-"] { - font-size: 14px; - margin: 0 !important; -} - -.token.tag { - background-color: transparent; - border-radius: 0; - color: #905; - line-height: auto; - margin: 0; - padding: 0; -} - -.token.tag:before, -.token.tag:after { - display: none; -} - -.token.operator { - background: none !important; -} - -.header { - background-color: #fff; - z-index: 2000; -} - -@media (min-width: 576px) { - .header { - padding: 16px 0; - } -} - -.header.is-visible { - top: 0; -} - -.header-inner { - align-items: center; - display: flex; - justify-content: space-between; -} - -.header-logo { - align-items: center; - display: flex; - flex-shrink: 0; - position: relative; -} - -.header-logoImage { - margin-right: 8px; -} - -@media (min-width: 576px) { - .header-logoImage { - margin-right: 8px; - } -} - -.header-nav { - align-items: center; - display: flex; - margin: 0; - margin-right: -12px; - overflow-x: auto; - padding: 20px 16px 20px 8px; - white-space: nowrap; -} - -@media (min-width: 576px) { - .header-nav { - margin-right: 0; - padding: 0; - } -} - -.header-navItem { - margin: 0; - margin-right: 16px; -} - -.header-navItem:last-child { - margin-right: 0; -} - -@media (max-width: 767px) { - .hide-small { - display: none; - } -} - -[v-cloak] { - display: none; -} - -/** - * Reset - */ -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; -} - -/** - * MAIN RULES - */ -*, -*::after, -*::before { - box-sizing: border-box; - outline: none; -} - -body { - background-color: #fff; - min-height: 100%; - overflow-x: hidden; - position: relative; -} - -p { - font-weight: normal; - margin-bottom: 1.5em; -} - -img { - max-width: 100%; -} - -strong { - font-weight: 600; -} - -ul { - margin-bottom: 1em; -} - -li { - list-style: none; - margin-bottom: 0.5em; -} - -.leaflet-popup-close-button { - visibility: hidden; -} - -.p-abs { - position: absolute; -} - -.list-group { - overflow: auto; - white-space: nowrap; - scrollbar-width: none; - padding: 1rem 0rem; -} - -.list-group-item { - border: 1px solid rgba(0, 0, 0, 0.125); - display: inline-block; - position: relative; -} - -.list-group-item div { - display: block; - padding: 0.5rem 1.5rem; -} diff --git a/router/api.js b/router/api.js deleted file mode 100644 index 1d8e13a..0000000 --- a/router/api.js +++ /dev/null @@ -1,103 +0,0 @@ -const axios = require("axios"); - -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 { - return reply.send([]); - } - return reply; - }); - fastify.get("/place/:id", async (req, reply) => { - const ENDPOINT = "https://nominatim.openstreetmap.org/"; - const FORMAT = "jsonv2"; - if (req.params.id) { - axios - .get(ENDPOINT, { - params: { - format: FORMAT, - q: req.params.id, - }, - }) - .then((res) => reply.send(res.data)); - } else { - return reply.send([]); - } - return reply; - }); - - fastify.get("/gpx/:id", async (req, reply) => { - if (req.params.id == undefined) - return reply.code(400).send({ error: "No ID query parameter" }); - - fastify.level.db.get(req.params.id, (err, val) => { - if (err) { - console.warn(err); - reply.code(500).send(); - } else { - let file = '' - const data = JSON.parse(val); - const gen_wpt = (name,desc,latlon,icon="Flag") => `0${name}-${desc}${icon}` - const esc_str = (str) => (str||"Undefined").replace('"',""").replace("'","'").replace("<","<").replace(">",">").replace("&","&").replace("\n","...") - data.main.forEach(a => { - file+= gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, icon="Hotel"); - a.places.restaurants.forEach(b => { - file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Restaurant"); - }); - a.places.activities.forEach(b => { - file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Tree"); - }); - }); - file+=""; - reply.header('Content-Type', 'application/gpx+xml'); - reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`); - reply.send(file); - } - }); - return reply; - }); - - fastify.get("/:id", async (req, reply) => { - if (req.params.id == undefined) - return reply.code(400).send({ error: "No ID query parameter" }); - - fastify.level.db.get(req.params.id, (err, val) => { - if (err) { - console.warn(err); - reply.code(500).send(); - } else { - reply.send(JSON.parse(val)); - } - }); - return reply; - }); - - fastify.post("/:id", async (req, reply) => { - if (req.params.id == undefined) - return reply.code(400).send({ error: "No ID query parameter" }); - - fastify.level.db.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" }); - } - }); - return reply; - }); - - done(); -}; diff --git a/server.js b/server.js deleted file mode 100644 index bffdc7c..0000000 --- a/server.js +++ /dev/null @@ -1,35 +0,0 @@ -const fastify = require("fastify")(); //{ logger: true }); -const path = require("path"); - -fastify.register(require("@fastify/static"), { - root: path.join(__dirname, "public"), - prefix: "/public/", -}); - -fastify.register( - require("@fastify/leveldb"), - { - name: "db", - }, - (err) => { - if (err) throw err; - } -); - -fastify.register(require("@fastify/view"), { - engine: { - pug: require("pug"), - }, -}); - -fastify.register(require("./router/api"), { prefix: "/api" }); - -fastify.get("/", (req, reply) => reply.view("/template/home.pug")); -fastify.get("/:id", (req, reply) => reply.view("/template/journey.pug")); -fastify.get("/view/:id", (req, reply) => reply.view("/template/view.pug")); -fastify.get("/short/:id", (req, reply) => reply.view("/template/short.pug")); - -fastify.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => { - if (err) throw err; - console.log("Listening on", address); -}); diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index f13157f..0000000 --- a/src/api.ts +++ /dev/null @@ -1,122 +0,0 @@ -import axios from "axios"; - -export const load = (id: string) => - axios.get("/api/" + id).then((response) => { - if (response.data == "") throw "Invalid Journey Data Received"; - let res = response.data; - - for (let e of res.main) { - if (e.dateRange) { - e.dateRange[0] = new Date(e.dateRange[0]); - e.dateRange[1] = new Date(e.dateRange[1]); - } - e.step_title = e.step_title || []; - } - return res; - }); - -export const save = (id: string, v: string) => - axios - .post("/api/" + id, v) - .then((response) => { - console.log("Saved..."); - }) - .catch((error) => { - console.warn("Error! Could not reach the API."); - }); - -export const query_nominatim = ( - q: string, - f: (v: string) => Boolean = () => true, -) => - axios - .get("/api/place/" + q) - .then((res) => res.data) - .then((res) => res.filter(f)); - -export const query_flight = (q: string) => - axios.get("/api/flight/" + q).then((res) => res.data); - -type NominatimResult = { - type: string; - category: string; - display_name: string; // DEBUG ONLY -}; - -export const is_restauration_type = (e: NominatimResult) => - ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf( - e.type, - ) != -1; - -export const is_attraction_type = (e: NominatimResult): boolean => - [ - "tourism", - "leisure", - "place", - "amenity", - "highway", - "historic", - "natural", - "waterway", - ].indexOf(e.category) != -1 || - [ - "place_of_worship", - "national_park", - "nature_reserve", - "protected_area", - ].indexOf(e.type) != -1; - -export const icon_type = (item: NominatimResult): string => { - let t = item.type; - let c = item.category; - const arr = ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"]; - if (arr.indexOf(t) != -1) { - return "utensils"; - } else if (t == "hotel" || t == "hostel" || t == "guest_house") { - return "bed"; - } else if (t == "museum" || c == "historic" || t == "place_of_worship") { - return "landmark"; - } else if (t == "peak" || t == "viewpoint") { - return "mountain"; - } else if (t == "parking") { - return "parking"; - } else if ( - t == "water" || - t == "river" || - t == "lake" || - t == "torrent" || - t == "aquarium" - ) { - return "water"; - } else if (t == "community_centre" || t == "locality") { - return "building"; - } else if (t == "attraction") { - return "landmark"; - } else if (t == "information" || t == "university") { - return "landmark"; - } else if (t == "bridge") { - return "archway"; - } else if ( - t == "woodland" || - t == "shieling" || - t == "national_park" || - t == "zoo" || - t == "park" || - t == "garden" || - 0 - ) { - return "tree"; - } else if (t == "water_park" || t == "theme_park") { - return "dice-five"; - } else if ( - t == "?" || - t == "neighbourhood" || - t == "quarter" || - c == "highway" - ) { - return ""; - } else { - console.log(item.display_name, item.category, item.type); - return "question"; - } -}; diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index 524306e..0000000 --- a/src/app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import "./types/ext"; -import "./api"; -import "./old.js"; - -console.log("TEST"); - -if (false) { - console.log("B"); -} - -function test() { - console.log("CC"); -} diff --git a/src/client/api.ts b/src/client/api.ts new file mode 100644 index 0000000..a1dee6b --- /dev/null +++ b/src/client/api.ts @@ -0,0 +1,152 @@ +export const throttle = (func: () => void, wait: number) => { + var lastTime = 0; + var timeoutId: ReturnType | undefined; + var lastArgs: any[]; + + return function (...args: any[]) { + const now = Date.now(); + lastArgs = args; + + if (timeoutId) clearTimeout(timeoutId); + timeoutId = undefined; + if (now - lastTime >= wait) { + lastTime = now; + func.apply(this, lastArgs); + } else { + timeoutId = setTimeout( + () => { + lastTime = Date.now(); + func.apply(this, lastArgs); + }, + wait - (now - lastTime) + ); + } + }; +}; + +export const load = (id: string) => + fetch("/api/" + id) + .then((res) => { + if (!res.ok) throw new Error("Error " + res.statusText); + return res.json(); + }) + .then((res) => { + for (let e of res.main) { + if (e.date_range) { + e.date_range[0] = new Date(e.date_range[0]); + e.date_range[1] = new Date(e.date_range[1]); + } + e.day_title = e.day_title || []; + } + return res; + }); + +export const save = (id: string, v: journey) => + fetch("/api/" + id, { method: "post", body: JSON.stringify(v) }) + .then((res) => { + if (!res.ok) throw new Error("Error " + res.statusText); + return res.json(); + }) + .then((_res) => { + console.log("Saved..."); + }); + +export const query_nominatim = ( + q: string, + bb: any, + f: (v: string) => Boolean = () => true +) => { + if (q.length == 0) return Promise.resolve([]) + let url = new URL("/api/place/" + q, window.location.origin); + url.searchParams.append("id", q); + url.searchParams.append("bb", JSON.stringify(bb)); + return fetch(url) + .then((res) => (res.status == 200 ? res.json() : [])) + .then((res) => res.filter(f)); +}; + +export const query_flight = (q: string) => + fetch("/api/flight/" + q).then((res) => res.json()); + +type NominatimResult = { + type: string; + category: string; + display_name: string; // DEBUG ONLY +}; + +export const is_restauration_type = (e: NominatimResult) => + ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf( + e.type + ) != -1; +export const is_attraction_type = (e: NominatimResult): boolean => + [ + "tourism", + "leisure", + "place", + "amenity", + // "highway", + "historic", + "natural", + "waterway", + ].indexOf(e.category) != -1 || + [ + "place_of_worship", + "national_park", + "nature_reserve", + "protected_area", + ].indexOf(e.type) != -1 || is_travel_type(e); + +export const is_hotel_type = (e: NominatimResult): boolean => + ["hotel", "hostel", "guest_house"].indexOf(e.type) != -1 +export const is_travel_type = (e: NominatimResult): boolean => + ["bus_stop", "tram_stop", "station", , "aerodrome", "parking"].indexOf(e.type) != -1 + + +export const icon_type = (item: string | NominatimResult): string => { + if (typeof (item) == "string") { + return item + } + let t = item.type; + let c = item.category; + let types = { + utensils: [ + "restaurant", + "cafe", + "pub", + "bar", + "fast_food", + "food_court", + ], + bed: ["hotel", "hostel", "guest_house"], + landmark: [ + "museum", + "historic", + "place_of_worship", + "attraction", + "information", + "university", "science_park", "theatre", "opera" + ], + mountain: ["peak", "viewpoint"], + parking: ["parking"], + water: ["water", "river", "lake", "torrent", "aquarium"], + building: ["community_center", "locality"], + archway: ["bridge"], + tree: [ + "woodland", + "shieling", + "national_park", + "park", + "zoo", + "garden", + "nature_reserve", + ], + "dice-five": ["water_park", "theme_park", "casino"], + "": ["?", "neighbourhood", "quarter", "highway"], + }; + + for (let k in types) { + if (types[k].indexOf(t) >= 0 || types[k].indexOf(c) >= 0) return k; + } + console.log(item.display_name, item.category, item.type); + return "question"; +}; diff --git a/src/client/main.ts b/src/client/main.ts new file mode 100644 index 0000000..9c35ce8 --- /dev/null +++ b/src/client/main.ts @@ -0,0 +1,4 @@ +import "./types/ext"; +import "./types/format"; +import "./api"; +import "./old"; diff --git a/src/client/old.js b/src/client/old.js new file mode 100644 index 0000000..419b1dd --- /dev/null +++ b/src/client/old.js @@ -0,0 +1,291 @@ + +import * as api from "./api"; +import journey_wrapper from "./types/wrapper"; +import { migrator } from "./types/migration"; +import { getGeoLine } from "./types/geom"; + +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-polyline", window.Vue2Leaflet.LPolyline); +Vue.component("l-control-scale", window.Vue2Leaflet.LControlScale); + +const app = new Vue({ + el: "#app", + data: { + edit_active: ["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1, + journey: new journey_wrapper(window.location.pathname.split("/").pop() || String.gen_id(16)), + map_override: { active: false, elements: [] }, + query: { + type: "", res: [], load: false, sub: false, note: false, drawer: false, + }, + leg_nav: { + scrollInterval: null, + scrollDir: null + }, + impexp: "", + lang: { + format: "ddd D MMM", + formatLocale: { + firstDayOfWeek: 1, + }, + monthBeforeYear: true, + }, + }, + methods: { + start_journey: function () { window.location.href = "/" + this.journey.id }, + + compute_bb: function () { + if (!this.$refs.map) return undefined + const bounds = this.$refs.map.mapObject.getBounds(); + return [[bounds.getSouthWest().lng, bounds.getSouthWest().lat], + [bounds.getNorthEast().lng, bounds.getNorthEast().lat]] + }, + generate_rotation: function (index, list) { + if (index < 0 || index >= list.length) return 0; + const c0 = list[(index == 0) ? index : (index - 1)] + const c1 = list[(index == list.length - 1) ? index : (index + 1)] + const brng = Math.atan2(c1[1] - c0[1], c1[0] - c0[0]); + return `rotate:${brng - Math.PI / 2}rad`; + + }, + generate_marker: function (item, fcolor) { + return ` +
+ + + + ` + }, + generate_icon: function (item, fcolor = "", styling = "", classes = "") { + return ``; + }, + + + import_data: function () { + this.journey.data = Object.assign( + {}, + JSON.parse(this.impexp.toDecoded()), + ); + this.journey.data.main.forEach((e) => { + if (e.date_range) { + e.date_range[0] = new Date(e.date_range[0]); + e.date_range[1] = new Date(e.date_range[1]); + } + }); + }, + export_data: function () { + this.impexp = JSON.stringify(this.journey.data).toEncoded(); + }, + filter_selected: function (list, step) { + return list.filter((e) => + step ? e.step == this.journey.sel_day : 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); + }, + place_delete: function (f, idx) { + switch (f) { + case "hotel": return this.journey.leg_get().hotel = null; + case "restaurant": return this.journey.leg_get().places.restaurants.splice(idx, 1); + case "activities": return this.journey.leg_get().places.activities.splice(idx, 1); + case "other": return; + case "flight": return this.journey.leg_get().travel.splice(idx, 1); + default: return true; + } + }, + get_filter: function (f) { + switch (f) { + case "hotel": return api.is_hotel_type; + case "restaurant": return api.is_restauration_type; + case "place": return api.is_attraction_type; + case "other": + default: return () => true; + } + }, + + search_nominatim: function (f) { + return (q) => api.query_nominatim(q, this.compute_bb(), this.get_filter(f)).catch((_err) => []).then((r) => { + r.forEach((rr) => { + rr.latlon = [parseFloat(rr.lat), parseFloat(rr.lon)]; + rr.sname = rr.display_name.split(",")[0]; + }); + r = r.filter(e => { + if (this.journey.leg_get().hotel && this.journey.leg_get().hotel.osm_id == e.osm_id) return false; + if (this.journey.leg_get().places.restaurants.find(i => i.osm_id == e.osm_id)) return false; + if (this.journey.leg_get().places.activities.find(i => i.osm_id == e.osm_id)) return false; + return true + }) + this.query.load = false; + this.query.res = r; + return r + }); + }, + search_travel: function (f) { + return (q) => api.query_flight(q).then((r) => { + r.forEach(el => { + el.path = getGeoLine( + { lat: el.from_geo.lat, lng: el.from_geo.lon }, + { lat: el.to_geo.lat, lng: el.to_geo.lon }, { dist: 2_500_000 }).map(v => [v.lat, v.lng]) + el.type = "flight"; + }); + r = r.filter(e => { + if (this.journey.leg_get().travel.find(i => `${i.from}->${i.to}` == `${e.from}->${e.to}`)) return false; + return true + }) + this.query.load = false; + this.query.res = r; + return r; + }); + }, + + drawer_hover_item: function (item) { + if (item) { + this.map_override.active = true + if (item.type == 'flight') { + this.map_override.elements = [[item.from_geo.lat, item.from_geo.lon], [item.to_geo.lat, item.to_geo.lon]] + } else { + this.map_override.elements = [[item.lat, item.lon]] + } + } else { + this.map_override.active = false + } + }, + + drawer_click_item: function (item) { + const tpe = this.query.type; + this.query.res = []; + this.query.note = false; + this.query.type = null; + this.query.drawer = item ? true : false; + setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500); + this.query.sub = false; + this.drawer_hover_item() + if (item) { + item.step = -1; + switch (tpe) { + case 'hotel': return this.journey.leg_get().hotel = item; + case 'restaurant': return this.journey.leg_get().places.restaurants.push(item); + case 'place': return this.journey.leg_get().places.activities.push(item); + case 'other': return; + case 'flight': return this.journey.leg_get().travel.push(item); + } + } + }, + + search_active: function (q) { + const txt = q.target.value + this.query.load = true; + switch (this.query.type) { + case 'hotel': return this.search_hotel(txt); + case 'restaurant': return this.search_restaurant(txt); + case 'place': return this.search_place(txt); + case 'other': return this.search_other(txt); + case 'flight': return this.search_flight(txt); + } + }, + + search_enable: function (f) { + this.query.drawer = true; + setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500); + if (f == "notes") { + this.query.note = true; + this.query.type = null; + const query_in = document.getElementById('query_note') + setTimeout(() => query_in.focus(), 500); + return; + } + this.query.note = false; + this.query.type = f; + const query_in = document.getElementById('query_input') + setTimeout(() => query_in.focus(), 500); + this.search_active({ target: query_in }) + }, + + + sideScroll: function (element, direction, speed, step) { + this.leg_nav.scrollDir = direction + if (direction == 'none') return; + this.leg_nav.scrollInterval = setInterval(() => { + element.scrollLeft += (direction == 'left') ? -step : step; + }, speed); + }, + + keyboardEvent(e) { + if (e.which === 13) { + } + }, + nav_mousemove(e) { + const c = document.querySelector('.scroll-content') + const left = e.pageX - c.getBoundingClientRect().left; + const newDir = + left < c.offsetWidth * 0.1 ? 'left' : + (left > c.offsetWidth * 0.9 ? 'right' : 'none') + if (!this.leg_nav.scrollInterval || this.leg_nav.scrollDir != newDir) { + if (this.leg_nav.scrollInterval) clearInterval(this.leg_nav.scrollInterval) + this.sideScroll(c, newDir, 25, 10); + } + }, + nav_mouseleave(e) { + clearInterval(this.leg_nav.scrollInterval); + this.leg_nav.scrollDir = 'none' + this.leg_nav.scrollInterval = null + }, + + refreshTextAreaHeight(event) { + console.log("AAA", event.target.scrollHeight, event.target) + event.target.style['height'] = 'auto'; + event.target.style['height'] = event.target.scrollHeight + 'px'; + event.target.style['max-height'] = "100%"; + }, + }, + created: function () { + this.search_hotel = api.throttle(this.search_nominatim("hotel"), 1000) + this.search_restaurant = api.throttle(this.search_nominatim("restaurant"), 1000) + this.search_place = api.throttle(this.search_nominatim("place"), 1000) + this.save_data = api.throttle(() => { + this.impexp = JSON.stringify(this.journey.data).toEncoded(); + api.save(this.journey.id, this.journey.data); + }, 1000); + this.search_flight = api.throttle(this.search_travel("flight"), 2000) + + window.addEventListener("keydown", (e) => { + switch (e.key) { + case "ArrowLeft": + this.journey.day_prev(); + break; + case "ArrowRight": + this.journey.day_next(); + break; + default: + console.log(e.key); + } + }); + + api.load(this.journey.id).then((r) => { + app.journey.data = migrator(r) + }); + }, + watch: { + journey: { + handler: function (ndata, odata) { + if (this.edit_active) + this.save_data(); + }, + deep: true, + }, + }, +}); + + diff --git a/src/types/ext.ts b/src/client/types/ext.ts similarity index 69% rename from src/types/ext.ts rename to src/client/types/ext.ts index cddb2b8..0344b70 100644 --- a/src/types/ext.ts +++ b/src/client/types/ext.ts @@ -2,8 +2,10 @@ declare global { interface Date { toJSONLocal: () => string; + toLocal: () => string; } } + Date.prototype.toJSONLocal = function () { function addZ(n: number): string { return n <= 9 ? `0${n}` : `${n}`; @@ -13,7 +15,27 @@ Date.prototype.toJSONLocal = function () { addZ(this.getMonth() + 1), addZ(this.getDate()), ].join("-"); -}; +} + +Date.prototype.toLocal = function () { + return [["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.getDay()], + this.getDate(), + [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ][this.getMonth()]].join(" ") + ; +} // ARRAY EXTENTION declare global { @@ -34,27 +56,20 @@ Array.prototype.foldl = function (f: (x: T, acc: B) => B, acc: B): B { // STRING EXTENTION declare global { interface String { - btoa: () => String; toEncoded: () => String; toDecoded: () => String; } } -String.prototype.btoa = function () { - return window.btoa(this); -}; - String.prototype.toEncoded = function () { - return window.btoa( - Array.from(this as string, (c) => c.charCodeAt(0)).foldl( - (e, v) => v + String.fromCharCode(e), - "", - ), - ); + return window.btoa(encodeURIComponent(Array.from(this as string, (c) => c.charCodeAt(0)).foldl( + (e, v) => v + String.fromCharCode(e), + "", + ))) }; String.prototype.toDecoded = function () { - return Array.from(window.atob(this), (c) => c.charCodeAt(0)).foldl( + return Array.from(decodeURIComponent(window.atob(this as string)), (c) => c.charCodeAt(0)).foldl( (e, v) => v + String.fromCharCode(e), "", ); @@ -76,4 +91,4 @@ String.gen_id = function (length) { .join(""); }; -export {}; +export { }; diff --git a/src/client/types/format.ts b/src/client/types/format.ts new file mode 100644 index 0000000..a0f1ff9 --- /dev/null +++ b/src/client/types/format.ts @@ -0,0 +1,59 @@ + + +declare global { + interface LatLng { + lat: number + lng: number + } + interface geoloc { + latlon: [number, number] + notes: string + step: -1 + } + + interface map { + zoom: number + center: LatLng + } + + + interface leg { + title: string + day_title: string[] + date_range: [Date, Date] | null + map: map + travel: unknown[] + hotel: geoloc | null + places: { + restaurants: geoloc[] + activities: geoloc[] + } + notes: string + } + + interface journey { + fmt_ver: number + title: string + main: leg[] + } +} + +const leg_template: leg = { + title: "", + day_title: [], + map: { zoom: 2, center: { lng: 0, lat: 0 } }, + travel: [], + hotel: null, + places: { restaurants: [], activities: [] }, + notes: "", + date_range: null +} +const journey_template: journey = { + fmt_ver: 1, + title: "New Journey", + main: [leg_template], +} + + +export { map, geoloc, leg, journey } +export { journey_template, leg_template } \ No newline at end of file diff --git a/src/client/types/geom.ts b/src/client/types/geom.ts new file mode 100644 index 0000000..23a864a --- /dev/null +++ b/src/client/types/geom.ts @@ -0,0 +1,165 @@ +const ellipsoid = { + a: 6378137, + b: 6356752.3142, + f: 1 / 298.257223563 +}; + +function mod(n: number, p: number): number { + const r = n % p; + return r < 0 ? r + p : r; +} + +function wrap(degrees: number, max = 360) { + if (-max <= degrees && degrees <= max) { + return degrees; + } else { + return mod(degrees + max, 2 * max) - max; + } +} + +function dist(src: LatLng, dst: LatLng, itr = 100, mit = true): number { + const p1 = src, + p2 = dst; + const φ1 = toRadians(p1.lat), + λ1 = toRadians(p1.lng); + const φ2 = toRadians(p2.lat), + λ2 = toRadians(p2.lng); + const π = Math.PI; + const ε = Number.EPSILON; + + // allow alternative ellipsoid to be specified + const { a, b, f } = ellipsoid; + + const dL = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ. + const tanU1 = (1 - f) * Math.tan(φ1), + cosU1 = 1 / Math.sqrt(1 + tanU1 * tanU1), + sinU1 = tanU1 * cosU1; + const tanU2 = (1 - f) * Math.tan(φ2), + cosU2 = 1 / Math.sqrt(1 + tanU2 * tanU2), + sinU2 = tanU2 * cosU2; + + const antipodal = Math.abs(dL) > π / 2 || Math.abs(φ2 - φ1) > π / 2; + + let λ = dL, + sinλ: number | null = null, + cosλ: number | null = null; // λ = difference in longitude on an auxiliary sphere + let σ = antipodal ? π : 0, + sinσ = 0, + cosσ = antipodal ? -1 : 1, + sinSqσ: number | null = null; // σ = angular distance P₁ P₂ on the sphere + let cos2σₘ = 1; // σₘ = angular distance on the sphere from the equator to the midpoint of the line + let sinα: number | null = null, + cosSqα = 1; // α = azimuth of the geodesic at the equator + let C: number | null = null; + + let λʹ: number | null = null, + iterations = 0; + do { + sinλ = Math.sin(λ); + cosλ = Math.cos(λ); + sinSqσ = + cosU2 * sinλ * (cosU2 * sinλ) + + (cosU1 * sinU2 - sinU1 * cosU2 * cosλ) * (cosU1 * sinU2 - sinU1 * cosU2 * cosλ); + if (Math.abs(sinSqσ) < ε) { + break; // co-incident/antipodal points (falls back on λ/σ = L) + } + sinσ = Math.sqrt(sinSqσ); + cosσ = sinU1 * sinU2 + cosU1 * cosU2 * cosλ; + σ = Math.atan2(sinσ, cosσ); + sinα = (cosU1 * cosU2 * sinλ) / sinσ; + cosSqα = 1 - sinα * sinα; + cos2σₘ = cosSqα !== 0 ? cosσ - (2 * sinU1 * sinU2) / cosSqα : 0; // on equatorial line cos²α = 0 (§6) + C = (f / 16) * cosSqα * (4 + f * (4 - 3 * cosSqα)); + λʹ = λ; + λ = dL + (1 - C) * f * sinα * (σ + C * sinσ * (cos2σₘ + C * cosσ * (-1 + 2 * cos2σₘ * cos2σₘ))); + const iterationCheck = antipodal ? Math.abs(λ) - π : Math.abs(λ); + if (iterationCheck > π) { + throw new EvalError("λ > π"); + } + } while (Math.abs(λ - λʹ) > 1e-12 && ++iterations < itr); + + if (iterations >= itr) { + if (mit) { + return dist( + src, + { lat: dst.lat, lng: dst.lng - 0.01 }, + itr, + mit + ); + } else { + throw new EvalError(`Inverse vincenty formula failed to converge after ${itr} iterations + (start=${src.lat}/${src.lng}; dest=${dst.lat}/${dst.lng})`); + } + } + const uSq = (cosSqα * (a * a - b * b)) / (b * b); + const A = 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))); + const B = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))); + const Δσ = + B * + sinσ * + (cos2σₘ + + (B / 4) * + (cosσ * (-1 + 2 * cos2σₘ * cos2σₘ) - + (B / 6) * cos2σₘ * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σₘ * cos2σₘ))); + + const s = b * A * (σ - Δσ); // s = length of the geodesic + return s + +} + +function pointDistance(src: LatLng, dst: LatLng): number { + return dist( + { lat: src.lat, lng: wrap(src.lng, 180) }, + { lat: dst.lat, lng: wrap(dst.lng, 180) } + ); +} + +function toRadians(degree: number): number { + return (degree * Math.PI) / 180; +} + +function toDegrees(radians: number): number { + return (radians * 180) / Math.PI; +} + +function midpoint(src: LatLng, dst: LatLng): LatLng { + // φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ)² + cos²φ2⋅sin²Δλ ) ) + // λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ) + // midpoint is sum of vectors to two points: mathforum.org/library/drmath/view/51822.html + + const φ1 = toRadians(src.lat); + const λ1 = toRadians(src.lng); + const φ2 = toRadians(dst.lat); + const Δλ = toRadians(dst.lng - src.lng); + + // get cartesian coordinates for the two points + const A = { x: Math.cos(φ1), y: 0, z: Math.sin(φ1) }; // place point A on prime meridian y=0 + const B = { x: Math.cos(φ2) * Math.cos(Δλ), y: Math.cos(φ2) * Math.sin(Δλ), z: Math.sin(φ2) }; + + // vector to midpoint is sum of vectors to two points (no need to normalise) + const C = { x: A.x + B.x, y: A.y + B.y, z: A.z + B.z }; + + const φm = Math.atan2(C.z, Math.sqrt(C.x * C.x + C.y * C.y)); + const λm = λ1 + Math.atan2(C.y, C.x); + + return { lat: toDegrees(φm), lng: toDegrees(λm) }; +} + +function recursiveMidPoint(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number } = {}, curr = 0): LatLng[] { + const geom: LatLng[] = [src, dst]; + const mp = midpoint(src, dst); + const split_step = (opt.step != undefined && (opt.step > 0 || curr == 0)) + const split_dist = (opt.dist != undefined && (pointDistance(src, dst) > opt.dist || curr == 0)) + const next_opt = split_step ? { step: (opt.step || 0) - 1 } : { dist: opt.dist }; + if (split_step || split_dist) { + geom.splice(0, 1, ...recursiveMidPoint(src, mp, next_opt, curr + 1)); + geom.splice(geom.length - 2, 2, ...recursiveMidPoint(mp, dst, next_opt, curr + 1)); + } else { + geom.splice(1, 0, mp); + } + return geom; +} + +export function getGeoLine(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number }) { + return recursiveMidPoint(src, dst, opt, 1) +} \ No newline at end of file diff --git a/src/client/types/migration.ts b/src/client/types/migration.ts new file mode 100644 index 0000000..2ccf0c0 --- /dev/null +++ b/src/client/types/migration.ts @@ -0,0 +1,24 @@ +const FMT_VER_0 = 0 +const FMT_VER_LATEST = FMT_VER_0 + +function migrate_A_to_0(e: journey): journey { + e.title = (e as any).name; + e.main.forEach((v) => { + v.date_range = v.date_range || (v as any).dateRange; + v.day_title = v.day_title || (v as any).step_title; + v.places.activities = v.places.activities || (v as any).places.places; + v.travel = v.travel || []; + }) + console.log(e) + return e; +} + +export const migrator = (e: journey): journey => { + if (e.fmt_ver == FMT_VER_LATEST) return e; + switch (e.fmt_ver) { + case FMT_VER_0: break; // Update when FMT_VER_1 releases + default: + return migrate_A_to_0(e) + } + return e; +} \ No newline at end of file diff --git a/src/client/types/wrapper.ts b/src/client/types/wrapper.ts new file mode 100644 index 0000000..ade7429 --- /dev/null +++ b/src/client/types/wrapper.ts @@ -0,0 +1,144 @@ +import { journey_template, leg_template } from "./format" + +const date_day_diff = (d0: Date, d1: Date): number => + (d1.getTime() - d0.getTime()) / (1000 * 60 * 60 * 24) + +class journey_wrapper { + id: String + data: journey = journey_template; + sel_leg: number = 0; + sel_day: number = 0; + + constructor(id: String) { + this.id = id; + } + + leg_first = () => this.data.main[0] + leg_last = () => this.data.main[this.leg_count() - 1] + leg_count(): number { + return this.data.main.length; + } + leg_len(idx?: number): number { + let d = this.leg_get(idx == undefined ? this.sel_leg : idx).date_range; + return d ? date_day_diff(d[0], d[1]) + 1 : 1; + } + add_leg(): void { + if (this.data.main == undefined) this.data.main = []; + this.data.main.push(leg_template); + } + rm_leg(idx: number): void { + this.data.main.splice(idx, 1); + if (this.sel_leg == idx) this.leg_prev(); + if (this.sel_leg > this.data.main.length - 1) this.leg_next(); + } + tot_len(): number | "?" { + if (this.leg_count() == 0) return 0; + let lf = this.leg_first(), ll = this.leg_last(); + if (lf.date_range && ll.date_range) { + let d0 = lf.date_range[0] + let d1 = ll.date_range[1] + return date_day_diff(d0, d1); + } + return "?"; + } + leg_sel(idx: number): void { + this.sel_leg = idx; + this.sel_day = 0; + } + leg_get(idx?: number): leg { + return this.data.main[idx != undefined ? idx : this.sel_leg] + } + leg_next(): void { + this.sel_leg = Math.min(this.sel_leg + 1, this.leg_count() - 1); + this.sel_day = 0; + } + leg_prev(): void { + this.sel_leg = Math.max(this.sel_leg - 1, 0); + this.sel_day = 0; + } + day_next() { + this.sel_day += 1 + if (this.sel_day > this.leg_len() - 1) { + if (this.sel_leg < this.leg_count() - 1) { + this.leg_next() + this.sel_day = 0; + } else { + this.sel_day = this.leg_len() - 1; + } + } + } + day_prev() { + this.sel_day -= 1 + if (this.sel_day < 0) { + if (this.sel_leg > 0) { + this.leg_prev() + this.sel_day = this.leg_len() - 1; + } else { + this.sel_day = 0; + } + } + } + date_sel(): string { + if (this.sel_day < 0) return "?"; + let leg = this.leg_get() + if (!leg.date_range) + return "?"; + var date = new Date(leg.date_range[0]); + date.setDate(date.getDate() + this.sel_day); + return date.toLocal(); + } + date_tot() { + if (this.leg_count() == 0) return ""; + let lf = this.leg_first(), ll = this.leg_last(); + if (lf.date_range && ll.date_range) + return `${lf.date_range[0].toLocal()} - ${ll.date_range[1].toLocal()}`; + return "?"; + } + + date_update(idx: number) { + let date_range = this.leg_get(idx).date_range; + if (!date_range) return; + let start_end = [0, 0]; + let step_len = 0; + + let last_start = date_range[0]; + for (let i = idx - 1; i >= 0; --i) { + step_len = this.leg_len(i) - 1; + if (this.leg_get(i).date_range) { + start_end = [last_start.getDate() - step_len, last_start.getDate()]; + } else { + this.leg_get(i).date_range = [new Date(), new Date()]; + start_end = [last_start.getDate() - step_len, last_start.getDate()]; + } + let leg = this.leg_get(i) + if (leg.date_range) { + leg.date_range[0].setTime(last_start.getTime()); + leg.date_range[0].setDate(start_end[0]); + leg.date_range[1].setTime(last_start.getTime()); + leg.date_range[1].setDate(start_end[1]); + last_start = leg.date_range[0]; + } + } + + let last_end = date_range[1]; + for (let i = idx + 1; i < this.leg_count(); ++i) { + step_len = this.leg_len(i) - 1; + if (this.leg_get(i).date_range) { + start_end = [last_end.getDate(), last_end.getDate() + step_len]; + } else { + this.leg_get(i).date_range = [new Date(), new Date()]; + start_end = [last_end.getDate(), last_end.getDate() + step_len]; + } + let leg = this.leg_get(i) + if (leg.date_range) { + leg.date_range[0].setTime(last_end.getTime()); + leg.date_range[0].setDate(start_end[0]); + leg.date_range[1].setTime(last_end.getTime()); + leg.date_range[1].setDate(start_end[1]); + last_end = leg.date_range[1]; + } + } + } +} + +export default journey_wrapper; \ No newline at end of file diff --git a/src/old.js b/src/old.js deleted file mode 100644 index 07049a7..0000000 --- a/src/old.js +++ /dev/null @@ -1,344 +0,0 @@ -import * as api from "./api"; - -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("multiselect", window.VueMultiselect.default); -Vue.use(window.VueTextareaAutosize); - -const app = new Vue({ - el: "#app", - data: { - journey_edit: - ["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1, - journey_id: window.location.pathname.split("/").pop() || String.gen_id(16), - - journey_step_data: { day: 1, section: 0 }, - journey_data: { - name: "New Journey", - main: [], - }, - - query: { hotel: [], flight: [], nominatim: [] }, - querying: { hotel: false, flight: false, place: false, food: false }, - impexp: "", - lang: { - format: "ddd D MMM", - formatLocale: { - firstDayOfWeek: 1, - }, - monthBeforeYear: true, - }, - }, - 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({ - title: "?", - step_title: [], - map: { zoom: 2 }, - hotel: { latlon: [0, 0] }, - places: { restaurants: [], places: [] }, - }); - }, - step_len: function (idx) { - return this.journey_data.main[idx].dateRange - ? (this.journey_data.main[idx].dateRange[1] - - this.journey_data.main[idx].dateRange[0]) / - (1000 * 60 * 60 * 24) + - 1 - : 1; - }, - next_step: function () { - this.journey_step_data.day += 1; - let s = this.journey_step_data.section; - let cd = this.step_len(s); - - if (this.journey_step_data.day > cd) { - this.journey_step_data.section += 1; - if (this.journey_step_data.section >= this.journey_data.main.length) { - this.journey_step_data.section = this.journey_data.main.length - 1; - this.journey_step_data.day = cd; - } else { - this.journey_step_data.day = 1; - } - } - }, - prev_step: function () { - this.journey_step_data.day -= 1; - if (this.journey_step_data.day <= 0) { - this.journey_step_data.section -= 1; - if (this.journey_step_data.section < 0) { - this.first_step(); - } else { - let s = this.journey_step_data.section; - - let cd = this.step_len(s); - this.journey_step_data.day = cd; - } - } - }, - nextnext_step: function () { - this.journey_step_data.section += 1; - this.journey_step_data.day = 1; - if (this.journey_step_data.section >= this.journey_data.main.length) - this.first_step(); - }, - prevprev_step: function () { - this.journey_step_data.section -= 1; - this.journey_step_data.day = 1; - if (this.journey_step_data.section < 0) this.first_step(); - }, - first_step: function () { - this.journey_step_data.section = 0; - this.journey_step_data.day = 1; - }, - - active_date: function () { - if (this.journey_step_data.day < 0) return "?"; - if (!this.journey_data.main[this.journey_step_data.section].dateRange) - return "?"; - var date = new Date( - this.journey_data.main[this.journey_step_data.section].dateRange[0], - ); - date.setDate(date.getDate() + this.journey_step_data.day - 1); - return this.format_date(date); - }, - format_date: function (d) { - return ( - ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()] + - " " + - d.getDate() + - " " + - [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ][d.getMonth()] - ); - }, - - total_days: function () { - if (this.journey_data.main.length == 0) return 0; - try { - return ( - (this.journey_data.main[this.journey_data.main.length - 1] - .dateRange[1] - - this.journey_data.main[0].dateRange[0]) / - (1000 * 60 * 60 * 24) - ); - } catch { - return "?"; - } - }, - total_date: function () { - if (this.journey_data.main.length == 0) return ""; - try { - return `${this.format_date( - this.journey_data.main[0].dateRange[0], - )} - ${this.format_date( - this.journey_data.main[this.journey_data.main.length - 1] - .dateRange[1], - )}`; - } catch { - return "?"; - } - }, - update_date: function (idx) { - let dateRange = this.journey_data.main[idx].dateRange; - let start_end = [0, 0]; - let step_len = 0; - - let last_start = dateRange[0]; - for (let i = idx - 1; i >= 0; --i) { - step_len = this.step_len(i) - 1; - if (this.journey_data.main[i].dateRange) { - start_end = [last_start.getDate() - step_len, last_start.getDate()]; - } else { - this.journey_data.main[i].dateRange = [new Date(), new Date()]; - start_end = [last_start.getDate() - step_len, last_start.getDate()]; - } - this.journey_data.main[i].dateRange[0].setTime(last_start.getTime()); - this.journey_data.main[i].dateRange[0].setDate(start_end[0]); - this.journey_data.main[i].dateRange[1].setTime(last_start.getTime()); - this.journey_data.main[i].dateRange[1].setDate(start_end[1]); - last_start = this.journey_data.main[i].dateRange[0]; - } - - let last_end = dateRange[1]; - for (let i = idx + 1; i < this.journey_data.main.length; ++i) { - step_len = this.step_len(i) - 1; - if (this.journey_data.main[i].dateRange) { - start_end = [last_end.getDate(), last_end.getDate() + step_len]; - } else { - this.journey_data.main[i].dateRange = [new Date(), new Date()]; - start_end = [last_end.getDate(), last_end.getDate() + step_len]; - } - this.journey_data.main[i].dateRange[0].setTime(last_end.getTime()); - this.journey_data.main[i].dateRange[0].setDate(start_end[0]); - this.journey_data.main[i].dateRange[1].setTime(last_end.getTime()); - this.journey_data.main[i].dateRange[1].setDate(start_end[1]); - last_end = this.journey_data.main[i].dateRange[1]; - } - }, - - rm_section: function (idx) { - this.journey_data.main.splice(idx, 1); - if (this.journey_step_data.section == idx) { - this.prevprev_step(); - } - }, - sel_section: function (idx) { - this.journey_step_data.section = idx; - this.journey_step_data.day = 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)]; - r.sname = r.display_name.split(",")[0]; - }); - 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, fcolor) { - return L.AwesomeMarkers.icon({ - icon: api.icon_type(item) || "star", - prefix: "fa", - markerColor: fcolor || item.color || "blue", - }).createIcon().outerHTML; - }, - - save_data: function () { - this.impexp = JSON.stringify(this.journey_data).toEncoded(); - api.save(this.journey_id, this.journey_data); - }, - import_data: function () { - this.journey_data = Object.assign( - {}, - JSON.parse(this.impexp.toDecoded()), - ); - this.journey_data.main.forEach((e) => { - if (e.dateRange) { - e.dateRange[0] = new Date(e.dateRange[0]); - e.dateRange[1] = new Date(e.dateRange[1]); - } - }); - }, - export_data: function () { - this.impexp = JSON.stringify(this.journey_data).toEncoded(); - }, - filter_selected: function (list, step) { - return list.filter((e) => - step ? e.step == this.journey_step_data.day : 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); - }, - - keyboardEvent(e) { - if (e.which === 13) { - } - }, - }, - created: function () { - window.addEventListener("keydown", (e) => { - switch (e.key) { - case "ArrowLeft": - this.prev_step(); - break; - case "ArrowRight": - this.next_step(); - break; - default: - console.log(e.key); - } - }); - - api.load(this.journey_id).then((r) => (app.journey_data = r)); - - this.debounceSave = _.debounce(this.save_data, 500); - this.debounceSearch = { - hotel: _.debounce((q) => { - this.querying.hotel = true; - this.search_nominatim( - q, - (r) => - r.type == "hotel" || r.type == "hostel" || r.type == "guest_house", - ).then((r) => { - this.querying.hotel = false; - }); - }, 500), - restaurants: _.debounce((q) => { - this.querying.food = true; - this.search_nominatim(q, (r) => api.is_restauration_type(r)).then( - (r) => { - this.querying.food = false; - }, - ); - }, 500), - places: _.debounce((q) => { - this.querying.place = true; - this.search_nominatim(q, (r) => api.is_attraction_type(r)).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(); - }, - deep: true, - }, - }, -}); diff --git a/src/server/api.ts b/src/server/api.ts new file mode 100644 index 0000000..9b8b531 --- /dev/null +++ b/src/server/api.ts @@ -0,0 +1,90 @@ +//import { ProxyAgent, setGlobalDispatcher } from 'undici'; + +import { flight_get_data } from './api_flight' +import { nominatim_get_data } from './api_nominatim'; + +//setGlobalDispatcher(new ProxyAgent(process.env.HTTPS_PROXY as string)); + + +export default function (server, opts, done) { + server.get("/flight/:id", async (req, reply) => + flight_get_data(req.params.id) + .then(res => { + let wait_for_all: Promise[] = [] + res.forEach(r => { + wait_for_all.push(nominatim_get_data(r.from).then(geo => (r as any).from_geo = geo[0])); + wait_for_all.push(nominatim_get_data(r.to).then(geo => (r as any).to_geo = geo[0])); + }); + return Promise.all(wait_for_all).then(_ => res) + }) + .then(res => reply.send(res)) + ); + + server.get("/place/:id", async (req, reply) => + nominatim_get_data(req.params.id, JSON.parse(req.query.bb)) + .then(res => reply.send(res)) + ); + + server.get("/gpx/:id", async (req, reply) => { + if (req.params.id == undefined) + return reply.code(400).send({ error: "No ID query parameter" }); + + server.level.db.get(req.params.id, (err, val) => { + if (err) { + console.warn(err); + reply.code(500).send(); + } else { + let file = '' + const data = JSON.parse(val); + const gen_wpt = (name, desc, latlon, icon = "Flag") => `0${name}-${desc}${icon}` + const esc_str = (str) => (str || "Undefined").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">").replace("&", "&").replace("\n", "...") + data.main.forEach(a => { + file += gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, "Hotel"); + a.places.restaurants.forEach(b => { + file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Restaurant"); + }); + a.places.activities.forEach(b => { + file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Tree"); + }); + }); + file += ""; + reply.header('Content-Type', 'application/gpx+xml'); + reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`); + reply.send(file); + } + }); + return reply; + }); + + server.get("/:id", async (req, reply) => { + if (req.params.id == undefined) + return reply.code(400).send({ error: "No ID query parameter" }); + + server.level.db.get(req.params.id, (err, val) => { + if (err) { + console.warn(err); + reply.code(500).send(); + } else { + reply.send(JSON.parse(val)); + } + }); + return reply; + }); + + server.post("/:id", async (req, reply) => { + if (req.params.id == undefined) + return reply.code(400).send({ error: "No ID query parameter" }); + + server.level.db.put(req.params.id, req.body, (err) => { + if (err) { + console.warn(err); + reply.code(500).send({ error: "Error with DB" }); + } else { + reply.send({ content: "ok" }); + } + }); + return reply; + }); + + done(); +}; diff --git a/src/server/api_flight.ts b/src/server/api_flight.ts new file mode 100644 index 0000000..14f5977 --- /dev/null +++ b/src/server/api_flight.ts @@ -0,0 +1,86 @@ +import { JSDOM } from 'jsdom'; + +interface FlightData { + id: string; + date: string; + from: string; + to: string; + std: string; + atd: string | string[] | null; + sta: string; + ata: string | string[] | null; +} + +function clean_times(s: string): string | null { + if (s == "—" || s == "Scheduled") return null + if (s.indexOf("Estimated departure") == 0) return null + if (s.indexOf("Landed") == 0) return s.replace("Landed ", "") + return s +} + +export function flight_get_data(flightId: string): Promise { + const url = new URL(`https://www.flightradar24.com/data/flights/${flightId}`); + return fetch(url).then(res => res.text()).then(res => new JSDOM(res)).then(dom => { + const rows = dom.window.document.querySelectorAll('table tbody tr'); + const flightData: FlightData[] = []; + rows.forEach(row => { + const columns = row.querySelectorAll('td'); + /* + * 2/6 = Date/Duration + * 3/4 = From/To + * 7/8 = STD/ATD + * 9/11 = STA/ATA + */ + const flight: FlightData = { + id: flightId, + date: columns[2].textContent.trim(), + from: columns[3].textContent.trim(), + to: columns[4].textContent.trim(), + std: columns[7].textContent.trim(), + atd: clean_times(columns[8].textContent.trim()), + sta: columns[9].textContent.trim(), + ata: clean_times(columns[11].textContent.trim()) + }; + flightData.push(flight); + }); + + return groupByPair(flightData).map(v => groupByFrequency(v)); + }) +} + +function groupByPair(flightData: FlightData[]): FlightData[][] { + const flightMap: { [key: string]: FlightData[] } = {}; + flightData.forEach(flight => { + const key = `${flight.from}-${flight.to}-${flight.std}`; + if (!flightMap[key]) + flightMap[key] = []; + flightMap[key].push(flight); + }); + return Object.values(flightMap); +} + +function groupByFrequency(flightData: FlightData[]): FlightData { + let data = 'OOOOOOO'; // Initialize with no flights + const atdArray: string[] = []; + const ataArray: string[] = []; + + flightData.forEach(flight => { + const dayOfWeek = (new Date(flight.date).getDay() + 6) % 7; + data = data.substring(0, dayOfWeek) + 'X' + data.substring(dayOfWeek + 1); + if (flight.atd) + atdArray.push(flight.atd as string); + if (flight.ata) + ataArray.push(flight.ata as string); + }); + + return { + id: flightData[0].id, + date: data, + from: flightData[0].from, + to: flightData[0].to, + std: flightData[0].std, + sta: flightData[0].sta, + atd: atdArray, + ata: ataArray + }; +}; diff --git a/src/server/api_nominatim.ts b/src/server/api_nominatim.ts new file mode 100644 index 0000000..874b77c --- /dev/null +++ b/src/server/api_nominatim.ts @@ -0,0 +1,27 @@ + +function drop_fields(results) { + results.forEach(e => { + delete e.licence; + delete e.place_rank; + delete e.importance; + delete e.boundingbox; + }); + return results +} + +export function nominatim_get_data(id: string, bb: string[][] | null = null): Promise { + if (!id) return Promise.resolve([]) + + const url = new URL("https://nominatim.openstreetmap.org/search"); + url.searchParams.append('format', 'jsonv2') + url.searchParams.append('q', id) + url.searchParams.append('limit', '20') + if (bb) { + url.searchParams.append('viewbox', `${bb[0][0]},${bb[0][1]},${bb[1][0]},${bb[1][1]}`) + url.searchParams.append('bounded', `1`) + } + return fetch(url).then((res) => { + if (!res.ok) throw new Error("Nominatim Error") + return res.json().then(r => drop_fields(r)) + }) +} diff --git a/src/server/main.ts b/src/server/main.ts new file mode 100644 index 0000000..599b401 --- /dev/null +++ b/src/server/main.ts @@ -0,0 +1,36 @@ +import fastify from 'fastify' +import fastify_static from '@fastify/static' +import fastify_db from '@fastify/leveldb' +import fastify_view from '@fastify/view'; +import pug from 'pug' +import { join as pathJoin } from "path"; + +import api from "./api" + +const server = fastify(); //{ logger: true }); + +server.register(fastify_static, { + root: pathJoin(__dirname, "../public"), + prefix: "/public/", +}); + +server.register( + fastify_db as any, + { name: "db" } +); + +server.register(fastify_view, { + engine: { pug: pug }, +}); + +server.register(api, { prefix: "/api" }); + +server.get("/", (req, reply) => reply.view("/src/template/home.pug")); +server.get("/:id", (req, reply) => reply.view("/src/template/journey.pug")); +server.get("/view/:id", (req, reply) => reply.view("/src/template/view.pug")); +server.get("/short/:id", (req, reply) => reply.view("/src/template/short.pug")); + +server.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => { + if (err) throw err; + console.log("Listening on", address); +}); diff --git a/src/style/custom.css b/src/style/custom.css new file mode 100644 index 0000000..e9b5b85 --- /dev/null +++ b/src/style/custom.css @@ -0,0 +1,207 @@ +.leaflet-popup-close-button { + visibility: hidden; +} + +.p-abs { + position: absolute; +} + +.list-group { + overflow: auto; + white-space: nowrap; + scrollbar-width: none; + padding: 1rem 0rem; +} + +.list-group-item { + border: 1px solid var(--darkdark); + display: inline-block; + position: relative; + cursor: pointer; + text-align: center; + padding: 0.5rem 0.8rem; +} + +.list-group-item:hover { + filter: brightness(85%); +} + +.leaflet-control-attribution { + display: none; +} + +.display-none { + display: none; +} + +.col-0 { + flex: 0 0 0%; + max-width: 0%; + overflow: hidden; +} + +.map-container, +.drawer-container { + transition: flex 0.5s ease-in-out, max-width 0.5s ease-in-out; +} + +.map-container { + min-width: 52px; + height: 100%; +} + +.drawer-container { + height: 100%; + right: 0; + top: 0; + overflow: scroll; +} + +.travel-path-icon { + margin-left: -12px; + margin-top: -32px; +} + + +.ml-auto { + margin-left: auto; +} + +.mr-auto { + margin-right: auto; +} + +.input .mx-input { + height: 40px; +} + +.h-100 { + height: 100%; +} + +.map-menu { + position: absolute; + display: flex; + z-index: 1100; + right: 0; + flex-direction: column; + gap: 10px; + margin: 5px; +} + +.map-menu-top { + top: 0; +} + +.map-menu-bottom { + bottom: 0; +} + +.map-menu-center { + top: 50%; +} + +.padding-1 { + padding: 5px; +} + +.map-menu-item { + background-color: var(--darkdark); + padding: 5px; + border-radius: 50%; + cursor: pointer; +} + +.map-menu-item:hover { + filter: brightness(150%); +} + +.map-menu-sub { + display: flex; + flex-direction: row-reverse; + gap: 5px; +} + +.map-menu-item, +.map-menu-sub-item { + background-color: var(--darkdark); + padding: 5px; + border-radius: 50%; + cursor: pointer; + float: right; + width: 42px; + height: 42px; + align-content: center; + text-align: center; +} + +.vue2leaflet-map { + border-radius: var(--border-radius); +} + +.leaflet-popup-content { + margin: 10px 20px !important; + /* display: flex; + align-items: center; + justify-content: center; + width: auto !important; + max-width: 100%; */ +} + + +.leaflet-popup>.leaflet-popup-content-wrapper { + border-radius: var(--border-radius); + width: 350px; +} + +.leaflet-popup-button-group { + position: absolute; + top: 0; + right: 0; + display: flex; + flex-direction: row; + gap: 5px; + margin: 2px 6px; +} + +.leaflet-popup-button-group>a { + cursor: pointer; +} + +.leaflet-popup-button-group>a:hover { + cursor: pointer; + filter: brightness(150%); +} + +.leaflet-popup-button-group>a>i { + font-size: 1em; +} + +.query-result { + display: flex; + align-items: center; + border-radius: var(--border-radius); + cursor: pointer; +} + +.query-result:hover { + filter: brightness(85%); +} + +.scroll-content>div:first-child { + border-top-left-radius: var(--border-radius); + border-bottom-left-radius: var(--border-radius); +} + +.scroll-content>div:nth-last-child(3) { + border-top-right-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); +} + +.scroll-content>div:last-child { + border-radius: var(--border-radius); +} + +.mx-datepicker { + width: 100% !important; +} \ No newline at end of file diff --git a/src/style/define.css b/src/style/define.css new file mode 100644 index 0000000..e7bbaed --- /dev/null +++ b/src/style/define.css @@ -0,0 +1,19 @@ +:root { + --black: #030B12; + --darkdark: #0C1D2E; + --dark: #203A53; + --lightdark: #425F7C; + --light: #93A9BE; + --lightlight: #B6C5D5; + --white: #F0F3F7; + --orange: ##F5B97D; + --yellow: #F5F57D; + --green: #B9F57D; + --turquoise: #7DF5B9; + --blue: #7DB9F5; + --purple: #B97DF5; + --pink: #F57DB9; + --red: #F57D7D; + + --border-radius: 3px; +} \ No newline at end of file diff --git a/src/style/index.css b/src/style/index.css new file mode 100644 index 0000000..018e967 --- /dev/null +++ b/src/style/index.css @@ -0,0 +1,13 @@ +@import './define.css'; + +@import './module/input.css'; +@import './module/load_n_spin.css'; +@import './module/typography.css'; +@import './module/layout.css'; +@import './module/general.css'; + +@import './custom.css'; + +[v-cloak] { + display: none; +} \ No newline at end of file diff --git a/src/style/module/general.css b/src/style/module/general.css new file mode 100644 index 0000000..88a2419 --- /dev/null +++ b/src/style/module/general.css @@ -0,0 +1,202 @@ +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; +} + + +*, +*::after, +*::before { + box-sizing: border-box; + outline: none; +} + +body { + background-color: #fff; + min-height: 100%; + overflow-x: hidden; + position: relative; +} + +p { + font-weight: normal; + margin-bottom: 1.5em; +} + +img { + max-width: 100%; +} + +strong { + font-weight: 600; +} + +ul { + margin-bottom: 1em; +} + +li { + list-style: none; + margin-bottom: 0.5em; +} + +/** + * BACKGROUND + */ +.bg-primary { + background-color: var(--blue); +} + +.bg-dark { + background-color: var(--darkdark); +} + +.bg-secondary { + background-color: var(--pink); +} + +.bg-white { + background-color: var(--white); +} + +.bg-success { + background-color: var(--green); +} + +.bg-info { + background-color: var(--yellow); +} + +.bg-warning { + background-color: var(--orange); +} + +.bg-error { + background-color: var(--red); +} + +.bg-gray { + background-color: var(--lightdark); +} + +.bg-gray-light { + background-color: var(--lightlight); +} + + +.align { + align-items: center; + justify-content: center; +} + +.fleft { + float: left; +} + +.fright { + float: right; +} + +.clearfix ::after { + clear: both; + content: ""; + display: table; +} + +.no-wrap { + white-space: nowrap; +} + +.overflow-hidden { + overflow: hidden; +} + +.rounded { + border-radius: var(--border-radius); +} \ No newline at end of file diff --git a/src/style/module/input.css b/src/style/module/input.css new file mode 100644 index 0000000..5ebba66 --- /dev/null +++ b/src/style/module/input.css @@ -0,0 +1,362 @@ +input, +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + resize: none; +} + +label { + display: block; + font-weight: normal; +} + +input:-webkit-autofill { + box-shadow: 0 0 0 1000px #eceff1 inset; +} + +.textarea, +.input, +.select { + border: 1px solid var(--white); + border-radius: var(--border-radius); + box-shadow: none; + display: inline-block; + font-weight: normal; + overflow: hidden; +} + +.textarea :focus, +.input :focus, +.select :focus { + outline: none; +} + +.textarea.has-error, +.input.has-error, +.select.has-error { + background: #eceff1; + border: 1px solid #e74c3c; + margin-bottom: 0; +} + +.select { + background-color: #eceff1; + display: inline-block; + margin-right: 16px; + position: relative; +} + +.select:last-child { + margin-right: 0; +} + +.select-fullWidth { + display: block; + margin-left: 0; + margin-right: 0; + width: 100%; +} + +.select select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-color: transparent; + border: 0; + border-radius: 0; + color: #272727; + display: block; + font-size: 16px; + line-height: 1.5em; + margin: 0; + padding: 8px 16px; + padding-right: 30px; + transition: background-color 0.2s ease-in-out; + width: 100%; +} + +.select select:active, +.select select:focus { + background-color: #fbfbfc; + border: 0; + outline: none; +} + +.select select::-ms-expand { + display: none; +} + +.select::after, +.select::before { + background: #03a9f4; + content: ""; + display: block; + height: 2px; + margin-top: 2px; + position: absolute; + right: 5px; + top: 50%; + -webkit-transform-origin: 1px; + transform-origin: 1px; + width: 10px; +} + +.select::after { + -webkit-transform: rotate(-135deg); + transform: rotate(-135deg); +} + +.select::before { + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +.textarea { + background-color: #eceff1; + padding: 0; +} + +.textarea-fullWidth { + display: block; + margin-left: 0; + margin-right: 0; + width: 100%; +} + +.textarea textarea { + background: transparent; + border: 0; + color: #272727; + display: block; + font-family: "Lato", sans-serif; + font-size: 16px; + line-height: 1.5em; + margin: 0; + min-height: 120px; + padding: 8px 16px; + transition: background-color 0.2s ease-in-out; + width: 100%; +} + +.textarea textarea::-webkit-input-placeholder { + color: #969da6; +} + +.textarea textarea::-ms-input-placeholder { + color: #969da6; +} + +.textarea textarea::placeholder { + color: #969da6; +} + +.textarea textarea:focus, +.textarea textarea:active { + background-color: #fbfbfc; + border: 0; + outline: none; +} + +.checkbox { + margin-bottom: 8px; + position: relative; +} + +.checkbox input[type="checkbox"] { + display: none; +} + +.checkbox input[type="checkbox"]:checked+label::after { + -webkit-animation: checkboxAndRadioAnimation 0.25s; + animation: checkboxAndRadioAnimation 0.25s; + content: ""; + -webkit-transform: scale(1) rotate(45deg); + transform: scale(1) rotate(45deg); +} + +.checkbox input[type="checkbox"]+label { + display: block; + overflow: hidden; + padding-left: 30px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.checkbox input[type="checkbox"]+label::before { + background-color: #eceff1; + border: 1px solid var(--white); + border-radius: var(--border-radius); + content: ""; + display: inline-block; + height: 20px; + left: 0; + margin-top: -10px; + position: absolute; + top: 50%; + width: 20px; +} + +.checkbox input[type="checkbox"]+label::after { + border-bottom: 3px solid #03a9f4; + border-right: 3px solid #03a9f4; + display: block; + height: 12px; + left: 11px; + margin-left: -4px; + margin-top: -7px; + position: absolute; + top: 50%; + width: 7px; + z-index: 1; +} + +.radio { + margin-bottom: 8px; + position: relative; +} + +.radio input[type="radio"] { + display: none; +} + +.radio input[type="radio"]:checked+label::after { + -webkit-animation: checkboxAndRadioAnimation 0.25s; + animation: checkboxAndRadioAnimation 0.25s; + content: ""; + -webkit-transform: scale(1) rotate(45deg); + transform: scale(1) rotate(45deg); +} + +.radio input[type="radio"]+label { + display: block; + overflow: hidden; + padding-left: 30px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.radio input[type="radio"]+label::before { + background-color: #eceff1; + border: 1px solid var(--white); + border-radius: 20px; + content: ""; + display: inline-block; + height: 20px; + left: 0; + margin-top: -10px; + position: absolute; + top: 50%; + width: 20px; +} + +.radio input[type="radio"]+label::after { + background-color: #03a9f4; + border-radius: 20px; + display: block; + height: 10px; + left: 11px; + margin-left: -6px; + margin-top: -6px; + position: absolute; + top: 13px; + width: 10px; + z-index: 1; +} + +@-webkit-keyframes checkboxAndRadioAnimation { + 0% { + -webkit-transform: scale(0) rotate(45deg); + transform: scale(0) rotate(45deg); + } + + 50% { + -webkit-transform: scale(1.5) rotate(45deg); + transform: scale(1.5) rotate(45deg); + } + + 100% { + -webkit-transform: scale(1) rotate(45deg); + transform: scale(1) rotate(45deg); + } +} + +@keyframes checkboxAndRadioAnimation { + 0% { + -webkit-transform: scale(0) rotate(45deg); + transform: scale(0) rotate(45deg); + } + + 50% { + -webkit-transform: scale(1.5) rotate(45deg); + transform: scale(1.5) rotate(45deg); + } + + 100% { + -webkit-transform: scale(1) rotate(45deg); + transform: scale(1) rotate(45deg); + } +} + +.input-invis { + background-color: transparent !important; + margin: auto !important; + border: 0 !important; +} + +.input { + background-color: var(--white); + padding: 0; + position: relative; +} + +.input :focus, +.input :active { + background-color: var(--white); + border-radius: var(--border-radius); +} + +.input input, +.input textarea { + background: transparent; + border: 0; + box-shadow: none; + color: #272727; + font-size: 16px; + line-height: 1.5em; + margin: 0; + outline: none; + padding: 8px 16px; + width: 100%; +} + +.input input::-webkit-input-placeholder { + color: #969da6; +} + +.input input::-ms-input-placeholder { + color: #969da6; +} + +.input input::placeholder { + color: #969da6; +} + +.input input.small { + line-height: 1em; + padding: 0; +} + +.input-withIcon input { + padding-right: 32px; +} + +.input-icon { + fill: #969da6; + height: 16px; + margin-top: -8px; + position: absolute; + right: 16px; + top: 50%; + width: 16px; +} \ No newline at end of file diff --git a/src/style/module/layout.css b/src/style/module/layout.css new file mode 100644 index 0000000..58ec6d9 --- /dev/null +++ b/src/style/module/layout.css @@ -0,0 +1,354 @@ +/** + * LAYOUT + */ +.section { + padding-bottom: 36px; + padding-top: 36px; +} + +@media (min-width: 768px) { + .section { + padding-bottom: 72px; + padding-top: 72px; + } +} + +.section+.section { + padding-top: 0; +} + +.container { + margin: 0 auto; + max-width: 1380px; + padding-left: 12px; + padding-right: 12px; + width: 100%; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +@media (min-width: 768px) { + .container { + padding-left: 24px; + padding-right: 24px; + max-width: 720px; + } +} + +.container-medium { + margin: 0 auto; + max-width: 944px; + padding-left: 12px; + padding-right: 12px; +} + +@media (min-width: 768px) { + .container-medium { + padding-left: 24px; + padding-right: 24px; + } +} + +.container { + width: 100%; + padding-right: 12px; + padding-left: 12px; + margin-right: auto; + margin-left: auto; +} + +.container-fluid { + width: 100%; + padding-right: 12px; + padding-left: 12px; + margin-right: auto; + margin-left: auto; +} + +.row { + display: flex; + flex-wrap: wrap; + margin-right: -12px; + margin-left: -12px; +} + +.col-1, +.col-2, +.col-3, +.col-4, +.col-5, +.col-6, +.col-7, +.col-8, +.col-9, +.col-10, +.col-11, +.col-12, +.col, +.col-auto, +.col-sm-1, +.col-sm-2, +.col-sm-3, +.col-sm-4, +.col-sm-5, +.col-sm-6, +.col-sm-7, +.col-sm-8, +.col-sm-9, +.col-sm-10, +.col-sm-11, +.col-sm-12, +.col-md-1, +.col-md-2, +.col-md-3, +.col-md-4, +.col-md-5, +.col-md-6, +.col-md-7, +.col-md-8, +.col-md-9, +.col-md-10, +.col-md-11, +.col-md-12 { + position: relative; + width: 100%; + min-height: 1px; + padding-right: 12px; + padding-left: 12px; +} + +.col { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; +} + +.col-auto { + flex: 0 0 auto; + width: auto; + max-width: none; +} + +.col-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; +} + +.col-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; +} + +.col-3 { + flex: 0 0 25%; + max-width: 25%; +} + +.col-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; +} + +.col-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; +} + +.col-6 { + flex: 0 0 50%; + max-width: 50%; +} + +.col-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; +} + +.col-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; +} + +.col-9 { + flex: 0 0 75%; + max-width: 75%; +} + +.col-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; +} + +.col-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; +} + +.col-12 { + flex: 0 0 100%; + max-width: 100%; +} + +@media (min-width: 576px) { + .col-sm { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .col-sm-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + + .col-sm-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + + .col-sm-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + + .col-sm-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-sm-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + + .col-sm-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + + .col-sm-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-sm-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + + .col-sm-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + + .col-sm-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-sm-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + + .col-sm-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; + } +} + +@media (min-width: 768px) { + .col-md { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + } + + .col-md-auto { + flex: 0 0 auto; + width: auto; + max-width: none; + } + + .col-md-1 { + flex: 0 0 8.33333%; + max-width: 8.33333%; + } + + .col-md-2 { + flex: 0 0 16.66667%; + max-width: 16.66667%; + } + + .col-md-3 { + flex: 0 0 25%; + max-width: 25%; + } + + .col-md-4 { + flex: 0 0 33.33333%; + max-width: 33.33333%; + } + + .col-md-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + + .col-md-6 { + flex: 0 0 50%; + max-width: 50%; + } + + .col-md-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + + .col-md-8 { + flex: 0 0 66.66667%; + max-width: 66.66667%; + } + + .col-md-9 { + flex: 0 0 75%; + max-width: 75%; + } + + .col-md-10 { + flex: 0 0 83.33333%; + max-width: 83.33333%; + } + + .col-md-11 { + flex: 0 0 91.66667%; + max-width: 91.66667%; + } + + .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + } +} \ No newline at end of file diff --git a/src/style/module/load_n_spin.css b/src/style/module/load_n_spin.css new file mode 100644 index 0000000..17fe58c --- /dev/null +++ b/src/style/module/load_n_spin.css @@ -0,0 +1,166 @@ +/** + * LOADING BAR + * + * Markup: + * --------- + *
+ * + */ +.loadingBar { + height: 6px; + left: 0; + overflow: hidden; + position: fixed; + right: 0; + top: 0; + width: 100%; + z-index: 1000; +} + +.loadingBar::before { + -webkit-animation: loading 2s linear infinite; + animation: loading 2s linear infinite; + background-color: #03a9f4; + content: ""; + display: block; + height: 6px; + left: -300px; + position: absolute; + width: 300px; +} + +@-webkit-keyframes loading { + from { + left: -300px; + width: 30%; + } + + 50% { + width: 30%; + } + + 70% { + width: 70%; + } + + 80% { + left: 50%; + } + + 95% { + left: 120%; + } + + to { + left: 100%; + } +} + +@keyframes loading { + from { + left: -300px; + width: 30%; + } + + 50% { + width: 30%; + } + + 70% { + width: 70%; + } + + 80% { + left: 50%; + } + + 95% { + left: 120%; + } + + to { + left: 100%; + } +} + +.spinner { + position: absolute; + right: 0; + top: 0; + width: 40px; + height: 40px; + display: block +} + +.spinner:after, +.spinner:before { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -12px 0 0 -12px; + width: 24px; + height: 24px; + border-radius: 100%; + border: 3px solid transparent; + border-top-color: var(--blue); +} + +.spinner:before { + -webkit-animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62); + animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62); + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite +} + +.spinner:after { + -webkit-animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8); + animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8); + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite +} + +@-webkit-keyframes spinning { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + + 25% { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + } + + 50% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + + 75% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes spinning { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + + 50% { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); + } + + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/style/module/typography.css b/src/style/module/typography.css new file mode 100644 index 0000000..96a1fa2 --- /dev/null +++ b/src/style/module/typography.css @@ -0,0 +1,148 @@ +/** + * TYPOGRAPHY + */ +body { + color: #272727; + font-family: "Lato", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 1.5em; +} + +a { + color: var(--blue); + text-decoration: none; +} + +a:hover { + color: color-mix(in srgb, var(--color-primary), #FFF 15%); +} + +a:focus { + color: var(--blue); +} + +.text-huge, +.text-big, +.text-medium { + margin-bottom: 1em; +} + +.text-huge { + font-size: 36px; + line-height: 1.3em; +} + +.text-big { + font-size: 24px; + line-height: 1.3em; +} + +.text-medium { + font-size: 16px; + line-height: 1.5em; +} + +.text-small { + font-size: 12px; + line-height: 1.3em; +} + +.text-body { + font-size: 16px; + line-height: 1.5em; +} + +.text-primary { + color: #03a9f4; +} + +.text-dark { + color: #18232f; +} + +.text-secondary { + color: #e91e63; +} + +.text-white { + color: #fff; +} + +.text-success { + color: #4caf50; +} + +.text-info { + color: #5bc0de; +} + +.text-warning { + color: #f0ad4e; +} + +.text-error { + color: #e74c3c; +} + +.text-gray { + color: #969da6; +} + +.text-gray-light { + color: #eceff1; +} + +.text-light { + font-weight: 300; +} + +.text-normal { + font-weight: 400; +} + +.text-lineThrough { + text-decoration: line-through; +} + +.text-italic { + font-style: italic; +} + +.text-underline { + text-decoration: underline; +} + +.text-uppercase { + text-transform: uppercase; +} + +.text-withSubtitle { + margin-bottom: 0 !important; +} + +.text-withSubtitle+.text-huge, +.text-withSubtitle+.text-big, +.text-withSubtitle+.text-medium, +.text-withSubtitle+.text-small { + margin-top: 0.5em; +} + +h1, +h2, +h3, +h4 { + font-weight: 300; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} \ No newline at end of file diff --git a/src/template/home.pug b/src/template/home.pug new file mode 100644 index 0000000..2a3d520 --- /dev/null +++ b/src/template/home.pug @@ -0,0 +1,32 @@ +doctype html + +include module/head.pug +main#app + .bg-white.section.text-dark + .container.mb-big + .text-center + div + h1.text-huge.text-withSubtitle Open Tourism Map + h2.text-big.text-gray Collaborative Holiday Planner + .spacer + .section.bg-dark.text-white + .container-medium + .row + .col-8 + h2.text-big Your journey + p.text-gray + | Browse hotels, restaurants and attractions,.... + br + | + | Select and plan the varying elements of your journey + .col-4 + .row.align + .input + input#journey.id(v-model="journey.id", placeholder="ID", type="text") + p + .row.align + button.button.button--primary.button--mobileFull( + v-on:click="start_journey" + ) Start the journey + +include module/foot.pug diff --git a/src/template/journey.pug b/src/template/journey.pug new file mode 100644 index 0000000..3b3e81b --- /dev/null +++ b/src/template/journey.pug @@ -0,0 +1,7 @@ +doctype html + +include module/head.pug + +main#app(v-cloak) + include module/journey/main.pug +include module/foot.pug diff --git a/src/template/module/foot.pug b/src/template/module/foot.pug new file mode 100644 index 0000000..6ea8792 --- /dev/null +++ b/src/template/module/foot.pug @@ -0,0 +1,13 @@ +script(src="https://unpkg.com/leaflet") + +script(src="https://unpkg.com/vue@2") +script(src="https://unpkg.com/vue2-datepicker") +script(src="https://unpkg.com/vue2-leaflet") +script(src="https://unpkg.com/sortablejs") +script(src="https://unpkg.com/vuedraggable") +script(src="/public/main.js", type="text/javascript", charset="utf-8") +footer.bg-dark.section + .container.text-center.text-small.text-white + | Built with   ❤   by Helcel + br + span.text-small.text-gray v0.0.1 diff --git a/template/module/head.pug b/src/template/module/head.pug similarity index 67% rename from template/module/head.pug rename to src/template/module/head.pug index 6252ff0..3274cfa 100644 --- a/template/module/head.pug +++ b/src/template/module/head.pug @@ -8,18 +8,10 @@ head href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700,300", type="text/css" ) - link(rel="stylesheet", href="/public/css/index.css") + link(rel="stylesheet", href="/public/index.css") link(rel="stylesheet", href="https://unpkg.com/vue2-datepicker/index.css") - link( - rel="stylesheet", - href="https://unpkg.com/vue-multiselect@2/dist/vue-multiselect.min.css" - ) link(rel="stylesheet", href="https://unpkg.com/leaflet/dist/leaflet.css") - 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/css/all.min.css" diff --git a/template/module/importexport.pug b/src/template/module/journey/impexp.pug similarity index 93% rename from template/module/importexport.pug rename to src/template/module/journey/impexp.pug index b66f61e..6e0c3a5 100644 --- a/template/module/importexport.pug +++ b/src/template/module/journey/impexp.pug @@ -1,4 +1,4 @@ -div +.impexp .container-medium.section .aligner .input.col-sm-4 @@ -10,4 +10,4 @@ div .col-sm-2 button.button.button--primary.button--mobileFull( v-on:click="export_data" - ) Export + ) Export \ No newline at end of file diff --git a/src/template/module/journey/leg/drawer-notes.pug b/src/template/module/journey/leg/drawer-notes.pug new file mode 100644 index 0000000..0f4a1c7 --- /dev/null +++ b/src/template/module/journey/leg/drawer-notes.pug @@ -0,0 +1,8 @@ +.col-12.input.text-dark() + textarea.text-small#query_note( + v-model="journey.leg_get().notes" + @input="refreshTextAreaHeight" + @focus="refreshTextAreaHeight" + rows="1" + placeholder="...", + ) \ No newline at end of file diff --git a/src/template/module/journey/leg/drawer.pug b/src/template/module/journey/leg/drawer.pug new file mode 100644 index 0000000..cc30f3e --- /dev/null +++ b/src/template/module/journey/leg/drawer.pug @@ -0,0 +1,48 @@ + +.col-12.input.text-dark + input#query_input( + type="search" + @input="search_active" + @focus="search_active" + placeholder="Search ... " + style="width:85%;" + :disabled="query.note" + ) + .spinner(v-if="query.load") + +div(v-if="['hotel', 'restaurant', 'place','other', 'travel'].indexOf(query.type)>=0") + template(v-for="(item, idx) in query.res" ) + .query-result.col-12.bg-white.text-dark( + :key="'q'+idx" + @mouseover="drawer_hover_item(item)" + @mouseleave="drawer_hover_item()" + @click="drawer_click_item(item)" ) + div( v-html="generate_icon(item, 'var(--dark)')") + .col-10() + | {{ item.name }} + .bg-dark.divider( + :key="'qdiv'+idx" style="height:1px" ) + .query-result.col-12.bg-white.text-dark( + v-if="query.load==false && query.res.length==0" ) + div( v-html="generate_icon('star', 'var(--dark)')") + .col-10() + | Add custom + + .col-12.text-white.text-center( + ) {{query.load? `Loading ...` : `Found ${query.res.length} results`}} +div(v-else-if="['flight'].indexOf(query.type)>=0") + template(v-for="(item, idx) in query.res" ) + .query-result.col-12.bg-white.text-dark( + :key="'q'+idx" + @mouseover="drawer_hover_item(item)" + @mouseleave="drawer_hover_item()" + @click="drawer_click_item(item)" ) + div( v-html="generate_icon('plane', 'var(--dark)')") + .col-10() + | {{ item.from }} => {{item.to}} + bg-dark.divider( + :key="'qdiv'+idx" style="height:1px" ) +div(v-else) + template() + .query-result.col-12.bg-white.text-dark() + | Unsuppored Query type {{query.type}} diff --git a/src/template/module/journey/leg/nav.pug b/src/template/module/journey/leg/nav.pug new file mode 100644 index 0000000..736c95e --- /dev/null +++ b/src/template/module/journey/leg/nav.pug @@ -0,0 +1,34 @@ +.scroll-handler.row( + @mouseleave="nav_mouseleave" + @mousemove="nav_mousemove") + + .col-3.col-sm-2.col-md-1 + .list-group.text-dark.h-100 + .fleft.list-group-item.bg-white.text-small.rounded.h-100(v-on:click.prevent="journey.leg_prev()") + i.fas.fa-angle-left + .col-6.col-sm-8.col-md-10 + draggable.scroll-content.list-group.bg-dark( + tag="div", + :list="journey.data.main", + handle=".handle" + ) + .list-group-item.handle.text-dark( + v-for="(element, idx) in journey.data.main", + :key="idx", + @click="journey.leg_sel(idx)", + :class="journey.sel_leg == idx ? 'bg-primary' : 'bg-white'" + ) + .text {{ element.title || "Leg "+idx}} + i.fa.fa-times.close.fright( + style="top: 2px; right: 2px; position: absolute", + @click="journey.rm_leg(idx)" + ) + .list-group-item.bg-dark + .list-group-item.bg-white.text-dark(@click="journey.add_leg()") + div + i.fa.fa-plus.add() + + .col-3.col-sm-2.col-md-1 + .list-group.text-dark.h-100 + a.fright.list-group-item.bg-white.text-small.rounded.h-100(v-on:click.prevent="journey.leg_next()") + i.fas.fa-angle-right \ No newline at end of file diff --git a/src/template/module/journey/leg/top.pug b/src/template/module/journey/leg/top.pug new file mode 100644 index 0000000..bd63bec --- /dev/null +++ b/src/template/module/journey/leg/top.pug @@ -0,0 +1,36 @@ +.row.text-center.align + .col-5.col-sm-4.col-md-2 + .input + input( + placeholder="Leg" + v-model="journey.leg_get().title") + + .col-5.col-sm-4.col-md-2.mr-auto + .input + input( + placeholder="Day" + v-model="journey.leg_get().day_title[journey.sel_day]" + ) + .col-8.col-sm-6.col-md-4 + .input + //- label Date Range ({{ journey.leg_len() }}) + date-picker( + :lang="lang", + v-model="journey.leg_get().date_range", + range="", + format="ddd D MMM", + placeholder="Date Range", + v-on:change="journey.date_update(journey.sel_leg)" + ) + .col-4.col-sm-4.col-md-3.ml-auto + .input + input( + disabled="", + :value="journey.date_sel() + ' (' + journey.sel_day + ')'" + ) + //- .col-6.list-group-item.align.center.bg-white(style="padding: 0.5rem 0;") + //- i.fas.fa-angle-double-right(v-on:click.prevent="journey.day_next()") + .col-sm-1.text-small + + //- a(href="#prev", v-on:click.prevent="journey.day_prev()") + i.fas.fa-angle-left \ No newline at end of file diff --git a/src/template/module/journey/main.pug b/src/template/module/journey/main.pug new file mode 100644 index 0000000..fb8d244 --- /dev/null +++ b/src/template/module/journey/main.pug @@ -0,0 +1,31 @@ +.row.fleft(style="position:absolute;right:0;") + .col-1 + a(:href="'/short/' + journey.id") + i.fas.fa-file-contract + .col-1 + a(:href="'/view/' + journey.id") + i.fas.fa-camera + .col-1 + a(href="#", v-on:click.prevent="first_step") + i.fas.fa-tools + + +.bg-dark.text-white(v-if="journey && journey.leg_get()") + .container + .row.align(style="padding-top:45px;") + .col-6.col-sm-4.col-md-3.input.text-big + input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text") + //- input.small(type="text", :placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'" ) + + include leg/nav.pug + include leg/top.pug + .row(style="aspect-ratio:1.25;") + .map-container(:class=" { 'col-2 col-sm-5 col-md-8': query.type, 'col-2 col-sm-5 col-md-6': query.note , 'col-12': (!query.type && !query.note) }" ) + include map.pug + .row.drawer-container(:class="{ 'col-10 col-sm-7 col-md-4': query.type, 'col-10 col-sm-7 col-md-6': query.note, 'col-0': (!query.type && !query.note) }") + .drawer-container(:class="{ 'col-12 ': query.type, 'col-0': !query.type }") + include leg/drawer.pug + .drawer-container(:class="{ 'col-12': query.note, 'col-0': !query.note }") + include leg/drawer-notes.pug + +//- include impexp.pug \ No newline at end of file diff --git a/src/template/module/journey/map.pug b/src/template/module/journey/map.pug new file mode 100644 index 0000000..5087f8a --- /dev/null +++ b/src/template/module/journey/map.pug @@ -0,0 +1,21 @@ +l-map( + :zoom.sync="journey.leg_get().map.zoom", + :center.sync="journey.leg_get().map.center", + style="height:100%" + no-blocking-animations=true + ref="map" +) + l-tile-layer( + url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + attribution="© OpenStreetMap contributors" + ) + l-control-scale(position="bottomleft", :imperial="false", :metric="true") + include map/override.pug + + include map/hotel.pug + include map/activities.pug + include map/restaurants.pug + + include map/travel.pug + template(v-if="edit_active") + include map/right_menu.pug \ No newline at end of file diff --git a/src/template/module/journey/map/activities.pug b/src/template/module/journey/map/activities.pug new file mode 100644 index 0000000..a7bb1c5 --- /dev/null +++ b/src/template/module/journey/map/activities.pug @@ -0,0 +1,6 @@ +include mixin-marker.pug +div( + v-for="(place, index) in journey.leg_get().places.activities", + :key="'activities'+index", + ) + +map_marker("activities", "var(--lightdark)", "var(--light)", "var(--lightlight)") \ No newline at end of file diff --git a/src/template/module/journey/map/hotel.pug b/src/template/module/journey/map/hotel.pug new file mode 100644 index 0000000..42947f7 --- /dev/null +++ b/src/template/module/journey/map/hotel.pug @@ -0,0 +1,7 @@ +include mixin-marker.pug +div( + v-if="journey.leg_get().hotel", + v-for="(place, index) in [journey.leg_get().hotel]", + :key="'hotel'+index", + ) + +map_marker("hotel", "var(--darkdark)", "var(--darkdark)", "var(--darkdark)") \ No newline at end of file diff --git a/src/template/module/journey/map/mixin-marker.pug b/src/template/module/journey/map/mixin-marker.pug new file mode 100644 index 0000000..9dc67e4 --- /dev/null +++ b/src/template/module/journey/map/mixin-marker.pug @@ -0,0 +1,37 @@ + +mixin map_marker(place, color_sel_c, color_sel_o, color_else) + l-marker( + :lat-lng="place.latlon" + ) + l-icon( + v-if="(place.step == journey.sel_day)" + v-html="generate_marker(place, \""+color_sel_c+"\")" + ) + l-icon( + v-else-if="(place.step >=0)" + v-html="generate_marker(place, \""+color_sel_o+"\")" + ) + l-icon( + v-else + v-html="generate_marker(place, \""+color_else+"\")" + ) + l-popup( + :options="{maxWidth:400, minWidth:300}") + h1.row.text-medium.text-center {{ place.sname }} + span.row.text-small.text-gray {{ place.display_name }} + span(v-if="edit_active") + .row.input() + textarea.col-12.col-sm-12.text-small( + placeholder="", + v-model="place.notes", + ) + .leaflet-popup-button-group(v-if="edit_active") + a.text-gray( + v-on:click.prevent="place.step = ((place.step==journey.sel_day)?-1:journey.sel_day)" + v-html="generate_icon(((place.step==journey.sel_day)?'calendar-xmark':'calendar-plus'), 'NA')" + ) + a.text-gray( + v-on:click.prevent="place_delete(\""+place+"\",index)" + v-html="generate_icon('trash', 'NA')" + ) + span.row.text-small.text-dark(v-else) {{ place.notes }} diff --git a/src/template/module/journey/map/override.pug b/src/template/module/journey/map/override.pug new file mode 100644 index 0000000..5fe6755 --- /dev/null +++ b/src/template/module/journey/map/override.pug @@ -0,0 +1,11 @@ +l-marker( + v-if="map_override.active", + v-for="(el, idx) in map_override.elements" + key="'ovr'+idx" + :lat-lng="el" + ) + l-icon(v-html="generate_marker('plus', 'darkgreen')") +l-polyline( + v-if="map_override.active && map_override.elements.length>1" + :lat-lngs="map_override.elements" :color="'darkgreen'" +) \ No newline at end of file diff --git a/src/template/module/journey/map/restaurants.pug b/src/template/module/journey/map/restaurants.pug new file mode 100644 index 0000000..1685cd8 --- /dev/null +++ b/src/template/module/journey/map/restaurants.pug @@ -0,0 +1,6 @@ +include mixin-marker.pug +div( + v-for="(place, index) in journey.leg_get().places.restaurants", + :key="'restaurants'+index", + ) + +map_marker("restaurants", "var(--dark)", "var(--dark)", "var(--dark)") \ No newline at end of file diff --git a/src/template/module/journey/map/right_menu.pug b/src/template/module/journey/map/right_menu.pug new file mode 100644 index 0000000..bd5990a --- /dev/null +++ b/src/template/module/journey/map/right_menu.pug @@ -0,0 +1,21 @@ +.map-menu.map-menu-top + div(v-if="query.type" @click="drawer_click_item()" ) + .map-menu-item(v-html="generate_icon('close')") + div(v-if="!query.type" @click="search_enable('hotel')") + .map-menu-item( v-html="generate_icon('bed')") + div(v-if="!query.type" @click="search_enable('restaurant')") + .map-menu-item( v-html="generate_icon('utensils')") + div(v-if="!query.type" @click="search_enable('place')") + .map-menu-item( v-html="generate_icon('star')") + .map-menu-sub(v-if="!query.type" @mouseenter="query.sub=true" @mouseleave="query.sub=false" ) + .map-menu-item(v-html="generate_icon('route')") + .map-menu-item(v-if="query.sub" @click="search_enable('flight')" v-html="generate_icon('plane')") + .map-menu-item(v-if="query.sub" @click="search_enable('train')" v-html="generate_icon('train')") + .map-menu-item(v-if="query.sub" @click="search_enable('car')" v-html="generate_icon('car')") + .map-menu-item(v-if="query.sub" @click="search_enable('other')" v-html="generate_icon('person-biking')") + +.map-menu.map-menu-center + div(v-if="query.note" @click="drawer_click_item()" ) + .map-menu-item(v-html="generate_icon('close')") + div(v-if="!query.note" @click="search_enable('notes')") + .map-menu-item( v-html="generate_icon('pencil')") \ No newline at end of file diff --git a/src/template/module/journey/map/travel.pug b/src/template/module/journey/map/travel.pug new file mode 100644 index 0000000..d7e0799 --- /dev/null +++ b/src/template/module/journey/map/travel.pug @@ -0,0 +1,32 @@ +mixin flight_popup() + l-popup( + :options="{maxWidth:400, minWidth:300}" + ) + h1.row.text-medium.text-center.text-uppercase {{ travel.id }} + span.row.text-small.text-gray {{ travel.from }} - {{travel.to}} + span(v-if="edit_active") + .row.input(style="margin-bottom:0") + textarea.col-12.col-sm-12.text-small( + placeholder="", + v-model="travel.notes", + ) + span.row.text-small.text-dark(v-else) {{ travel.notes }} + span(v-if="edit_active") + .leaflet-popup-button-group(v-if="edit_active") + a.text-gray( + v-on:click.prevent="place_delete('flight',idx)" + v-html="generate_icon('trash', 'NA')" + ) + +div(v-for= "(travel, idx) in journey.leg_get().travel") + l-polyline(:lat-lngs="travel.path" :color="travel.color || 'gray'") + +flight_popup() + + l-marker( + v-for="(place, index) in travel.path" + :key="'plane'+index" + :lat-lng="place" + ) + l-icon(v-html="generate_icon('plane', travel.color || 'gray', generate_rotation(index,travel.path), 'travel-path-icon')" + ) + +flight_popup() diff --git a/src/template/module/view/nav.pug b/src/template/module/view/nav.pug new file mode 100644 index 0000000..fa9eb3b --- /dev/null +++ b/src/template/module/view/nav.pug @@ -0,0 +1,10 @@ +.row.fleft(style="position:absolute;right:0;") + .col-1 + a(:href="'/short/' + journey.id") + i.fas.fa-file-contract + .col-1 + a(:href="'/view/' + journey.id") + i.fas.fa-camera + //- .col-1 + //- a(:href="'/' + journey.id" v-on:click.prevent="first_step") + //- i.fas.fa-tools diff --git a/src/template/module/view/short_leg.pug b/src/template/module/view/short_leg.pug new file mode 100644 index 0000000..756cabb --- /dev/null +++ b/src/template/module/view/short_leg.pug @@ -0,0 +1,39 @@ +.col-11.container.section + .row.text-center.align.padding-1 + .input.col-5.col-sm-2 + input(disabled="", placeholder="Unnamed" :value="item.title") + .col-sm-1 + .input.col-6.col-sm-4 + input( + disabled="", + placeholder="No Dates", + :value="item.date_range ? item.date_range[0].toLocal() + ' - ' + item.date_range[1].toLocal() : ''" + ) + .col-1.col-sm-2 + .input.col-5.col-sm-3.text-dark + .text(disabled="" placeholder="No Hotel" :value="item.hotel?item.hotel.sname:''") {{item.hotel?item.hotel.sname:'No Hotel'}} + //- .row.text-center + .input.col-sm-3(v-if="item.travel") + div(v-for="(item, idx) in item.travel") + input(disabled="", placeholder="-" :value="item.map((v) => v.id).join(', ')") + .row.align.padding-1 + .input.col-sm-10.text-dark + .text-small( + placeholder="No Restaurants", + :value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'", + disabled="" + ) {{item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'}} + .row.align.padding-1 + .input.col-sm-10.text-dark + .text-small( + placeholder="No Activities", + :value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'", + disabled="" + ) {{item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'}} + .row.align.padding-1 + .input.col-sm-10.text-dark + .text-small( + placeholder="No Notes", + :value="item.notes || 'No Notes'", + disabled="" + ) {{item.notes || 'No Notes'}} diff --git a/src/template/module/view/view_day.pug b/src/template/module/view/view_day.pug new file mode 100644 index 0000000..a90fcd6 --- /dev/null +++ b/src/template/module/view/view_day.pug @@ -0,0 +1,19 @@ +div(v-for="(e, idx) in journey.data.main", :key="idx") + .bg-dark.text-white(v-if="journey.sel_leg == idx") + .container.section + .row + .col-3.fleft.text-center.text-white.text-huge + a(v-on:click.prevent="journey.day_prev()") + i.fas.fa-angle-left + .col-6.container.text-center.align + span.small {{ journey.data.main[idx].title }} {{ journey.sel_day }} + .text-big.text-gray {{ journey.data.main[idx].day_title[journey.sel_day] }} + .col-3.fright.text-center.text-white.text-huge + a(v-on:click.prevent="journey.day_next()") + i.fas.fa-angle-right + .row + .col-12.col-sm-12(style="aspect-ratio:1.25;") + include ../journey/map.pug + .row + .col-10 + span.small.text-gray {{journey.data.main[idx].note || '...'}} diff --git a/src/template/short.pug b/src/template/short.pug new file mode 100644 index 0000000..61662f6 --- /dev/null +++ b/src/template/short.pug @@ -0,0 +1,16 @@ +doctype html +include module/head.pug +main#app(v-cloak) + include module/view/nav.pug + + .bg-dark.text-white(v-if="journey && journey.leg_get()") + .container + .row.align(style="padding-top:45px;") + .col-7.col-sm-5.col-md-4.input.text-big + input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text" disabled) + div( + v-for="(item, idx) in journey.data.main", + :class="idx % 2 === 0 ? 'bg-white text-dark' : 'bg-dark text-white'" + ) + include module/view/short_leg.pug +include module/foot.pug diff --git a/src/template/view.pug b/src/template/view.pug new file mode 100644 index 0000000..ff1fff4 --- /dev/null +++ b/src/template/view.pug @@ -0,0 +1,6 @@ +doctype html +include module/head.pug +main#app(v-cloak) + div(v-if="journey.leg_get()") + include module/view/view_day.pug +include module/foot.pug diff --git a/template/home.pug b/template/home.pug deleted file mode 100644 index 41087ea..0000000 --- a/template/home.pug +++ /dev/null @@ -1,77 +0,0 @@ -doctype html - -include module/head.pug -main#app - .container - section.mb-big - .text-center - img.main-logo.mb-medium( - src="/public/img/helcel.png", - alt="Helcel logo" - ) - div - h1.text-huge.text-withSubtitle Open Tourism Map - h2.text-big.text-gray Collaborative Holiday Planner - p#js-header-waypoint.m-none - a.button.button--primary.button--mobileFull(href="#go") Get started - .bg-dark - .container - .row.text-center - .col-12.col-sm-3 - .section - img( - src="/public/img/lightweight.png", - alt="Lightweight", - width="118" - ) - br - h2.text-withSubtitle.text-big.text-white - | Lightweight - br - span.text-medium.text-gray - | Powered By - br - | Fastify & Sierra - .col-12.col-sm-4 - .section - img( - src="/public/img/customizable.png", - alt="Customizable", - width="118" - ) - br - h2.text-withSubtitle.text-big.text-white - | Customizable - br - span.text-medium.text-gray - | Many Templates - br - | to choose from - .col-12.col-sm-4 - .section - h2.text-withSubtitle.text-big.text-white - img( - src="/public/img/opensource.png", - alt="Open Source", - width="118" - ) - br - | - | FOSS - br - span.text-medium.text-gray :-) - #go.container-medium.section - h2.text-big Your journey - p - | Browse hotels, restaurants and attractions,.... - br - | - | Select and plan the varying elements of your journey - .aligner.aligner--contentEnd - .input - input#journey_id(v-model="journey_id", placeholder="ID", type="text") - button.button.button--primary.button--mobileFull( - v-on:click="start_journey" - ) Start the journey - -include module/foot.pug diff --git a/template/journey.pug b/template/journey.pug deleted file mode 100644 index 3d9a79c..0000000 --- a/template/journey.pug +++ /dev/null @@ -1,10 +0,0 @@ -doctype html - -include module/head.pug - -main#app(v-cloak) - include module/nav.pug - include module/journey_sec.pug - include module/journey_step.pug - include module/importexport.pug -include module/foot.pug diff --git a/template/module/foot.pug b/template/module/foot.pug deleted file mode 100644 index 9c2437c..0000000 --- a/template/module/foot.pug +++ /dev/null @@ -1,26 +0,0 @@ -script(src="https://unpkg.com/leaflet") -script(src="https://unpkg.com/leaflet.awesome-markers") -//- script(src="https://unpkg.com/axios") -script(src="https://unpkg.com/lodash") -script(src="https://unpkg.com/sortablejs") - -script(src="https://unpkg.com/vue@2") -script(src="https://unpkg.com/vue2-datepicker") -script(src="https://unpkg.com/vue-textarea-autosize") -script(src="https://unpkg.com/vue-multiselect@2") -script(src="https://unpkg.com/vue2-leaflet") -script(src="https://unpkg.com/vuedraggable") -script(src="/public/main.js", type="text/javascript", charset="utf-8") -footer.bg-dark - .container - .section.text-center.text-small - p.text-white - img(src="/public/img/helcel.png", alt="helcel logo", width="100") - br - br - | - | Built with   ❤   by Helcel - br - span.text-small.text-gray v0.0.1 - p.text-gray - a(href="https://git.helcel.net") Helcel Git diff --git a/template/module/journey_sec.pug b/template/module/journey_sec.pug deleted file mode 100644 index 4b5640e..0000000 --- a/template/module/journey_sec.pug +++ /dev/null @@ -1,20 +0,0 @@ -draggable.list-group.bg-dark( - tag="div", - :list="journey_data.main", - handle=".handle" -) - .list-group-item.handle( - v-for="(element, idx) in journey_data.main", - :key="idx", - @click="sel_section(idx)", - :class="journey_step_data.section == idx ? 'bg-primary' : 'bg-white'" - ) - .text {{ element.title }} - i.fa.fa-times.close.fright( - style="top: 2px; right: 2px; position: absolute", - @click="rm_section(idx)" - ) - - .list-group-item.bg-white(@click="add_section()") - .text Add Section - i.fa.fa-plus.add(style="top: 12px; right: 5px; position: absolute") diff --git a/template/module/journey_step.pug b/template/module/journey_step.pug deleted file mode 100644 index 10281da..0000000 --- a/template/module/journey_step.pug +++ /dev/null @@ -1,102 +0,0 @@ -div(v-for="(e, idx) in journey_data.main", :key="idx") - .bg-dark.text-white(v-if="journey_step_data.section == idx") - .container.section - .row.text-center - .input.col-sm-2 - input(v-model="journey_data.main[idx].title") - .input.col-sm-2 - input( - placeholder="Day title", - v-model="journey_data.main[idx].step_title[journey_step_data.day]" - ) - .col-sm-3 - .right.input.col-sm-2 - input( - disabled="", - :value="active_date() + ' (' + journey_step_data.day + ')'" - ) - .row - .col-9.col-ssm-12 - include map.pug - .col-3.col-ssm-12 - .row.text-center - div - label Date Range ({{ step_len(idx) }}) - .input.text-dark - date-picker( - :lang="lang", - v-model="journey_data.main[idx].dateRange", - range="", - format="ddd D MMM", - placeholder="Date Range", - v-on:change="update_date(idx)" - ) - .row.text-center - div - label Hotel - multiselect#ajax( - v-model="journey_data.main[idx].hotel", - label="sname", - 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", - :options-limit="50", - :limit="1", - :max-height="600", - @search-change="debounceSearch.hotel" - ) - .row.text-center - div - label Restoration - multiselect#ajax( - v-model="journey_data.main[idx].places.restaurants", - label="sname", - 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", - :options-limit="50", - :limit="10", - :max-height="600", - @search-change="debounceSearch.restaurants" - ) - .row.text-center - div - label Activities - multiselect#ajax( - v-model="journey_data.main[idx].places.activities", - label="sname", - 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="50", - :limit="10", - :max-height="600", - @search-change="debounceSearch.places" - ) - .row.text-center - div - label Notes - .input.text-dark(style="width: 100%") - textarea-autosize.text-small( - v-model="journey_data.main[idx].notes", - placeholder="Notes", - :min-height="30", - :max-height="350" - ) diff --git a/template/module/map.pug b/template/module/map.pug deleted file mode 100644 index 34f6e83..0000000 --- a/template/module/map.pug +++ /dev/null @@ -1,84 +0,0 @@ -l-map( - :zoom.sync="journey_data.main[idx].map.zoom", - :center.sync="journey_data.main[idx].map.center", - style="padding-top: 100%" -) - l-tile-layer( - url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - attribution="© OpenStreetMap contributors" - ) - l-control-scale(position="topright", :imperial="false", :metric="true") - l-marker( - v-if="journey_data.main[idx].hotel", - :lat-lng="journey_data.main[idx].hotel.latlon" - ) - l-icon - div(v-html="generate_icon(journey_data.main[idx].hotel, 'darkblue')") - l-popup - h1.row.text-medium.text-center {{ journey_data.main[idx].hotel.sname }} - span.row.text-small.text-gray {{ journey_data.main[idx].hotel.display_name }} - span(v-if="journey_edit") - .row.input - textarea-autosize.col-12.col-sm-12.text-small( - placeholder="Notes", - v-model="journey_data.main[idx].hotel.notes", - :min-height="30", - :max-height="350" - ) - span.row.text-small.text-white(v-else) {{ journey_data.main[idx].hotel.notes }} - l-marker( - v-for="place in journey_data.main[idx].places.activities", - :lat-lng="place.latlon" - ) - l-icon - div( - v-if="place.step == journey_step_data.day", - v-html="generate_icon(place)" - ) - div( - v-else-if="place.step == -1 || place.step == undefined", - v-html="generate_icon(place, 'gray')" - ) - div(v-else-if="journey_edit", v-html="generate_icon(place, 'lightgray')") - div(v-else) - l-popup - h1.row.text-medium.text-center {{ place.sname }} - span.row.text-small.text-gray {{ place.display_name }} - span(v-if="journey_edit") - .row.input - textarea-autosize.col-12.col-sm-12.text-small( - placeholder="Notes", - v-model="place.notes", - :min-height="30", - :max-height="350" - ) - a.leaflet-popup-close-button.text-gray( - style="right: 0px; visibility: visible", - href="#rm", - v-on:click.prevent="place.step = -1" - ) - - a.leaflet-popup-close-button.text-gray( - style="right: 16px; visibility: visible", - href="#ad", - v-on:click.prevent="place.step = journey_step_data.day" - ) + - span.row.text-small.text-dark(v-else) {{ place.notes }} - - l-marker( - v-for="place in journey_data.main[idx].places.restaurants", - :lat-lng.sync="place.latlon" - ) - l-icon - div(v-html="generate_icon(place, 'cadetblue')") - l-popup - h1.row.text-medium.text-center {{ place.sname }} - span.row.text-small.text-gray {{ place.display_name }} - span(v-if="journey_edit") - .row.input - textarea-autosize.col-12.col-sm-12.text-small( - placeholder="Notes", - v-model="place.notes", - :min-height="30", - :max-height="350" - ) - span.row.text-small.text-dark(v-else) {{ place.notes }} diff --git a/template/module/nav.pug b/template/module/nav.pug deleted file mode 100644 index c7173f1..0000000 --- a/template/module/nav.pug +++ /dev/null @@ -1,38 +0,0 @@ -header.header - .header-inner.container - a.header-logo.text-dark(href="/") - img.header-logoImage( - src="/public/img/helcel.png", - alt="Helcel logo", - width="40" - ) - span.hide-small OTM - .input.input-invis.row - input.col-6.small(v-model="journey_data.name", type="text") - input.col-6.small( - disabled, - type="text", - :placeholder="total_date() + ' (' + total_days() + ')'" - ) - .row.header-nav.text-big(style="margin-bottom: 0") - .col-sm-2 - a(:href="'/short/' + journey_id") - i.fas.fa-file-contract - .col-sm-2 - a(:href="'/view/' + journey_id") - i.fas.fa-camera - .col-sm-2 - a(href="#main", v-on:click.prevent="first_step") - i.fas.fa-tools - .col-sm-1.text-small - a(href="#prevprev", v-on:click.prevent="prevprev_step") - i.fas.fa-angle-double-left - .col-sm-1 - a(href="#prev", v-on:click.prevent="prev_step") - i.fas.fa-angle-left - .col-sm-1 - a(href="#next", v-on:click.prevent="next_step") - i.fas.fa-angle-right - .col-sm-1.text-small - a(href="#nextnext", v-on:click.prevent="nextnext_step") - i.fas.fa-angle-double-right diff --git a/template/module/nav_pub.pug b/template/module/nav_pub.pug deleted file mode 100644 index 49af761..0000000 --- a/template/module/nav_pub.pug +++ /dev/null @@ -1,18 +0,0 @@ -header.header - .header-inner.container - a.header-logo.text-dark(href="/") - img.header-logoImage( - src="/public/img/helcel.png", - alt="Helcel logo", - width="40" - ) - span.hide-small HOTM - .input.input-invis - input.small(:value="journey_data.name", type="text", disabled="") - .row.header-nav.text-big(style="margin-bottom: 0") - .col-sm-3 - a(:href="'/short/' + journey_id") - i.fas.fa-file-contract - .col-sm-3 - a(:href="'/view/' + journey_id") - i.fas.fa-camera diff --git a/template/module/short_sec.pug b/template/module/short_sec.pug deleted file mode 100644 index 3bfe2bf..0000000 --- a/template/module/short_sec.pug +++ /dev/null @@ -1,43 +0,0 @@ -.container.section - .row.text-center - .input.col-sm-2 - input(disabled="", :value="item.title") - .input.col-sm-4 - input( - disabled="", - placeholder="No Dates", - :value="item.dateRange ? format_date(item.dateRange[0]) + ' - ' + format_date(item.dateRange[1]) : ''" - ) - .input.col-sm-2 - input(disabled="", placeholder="No Hotel", :value="item.hotel.sname") - .row.text-center - .input.col-sm-3(v-if="item.transit") - div(v-for="(item, idx) in item.transit") - input(disabled="", :value="item.map((v) => v.id).join(', ')") - .row.text-center - .input.col-sm-8(v-if="item.places && item.places.restaurants") - textarea-autosize.text-small( - placeholder="No Restaurants", - :value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')", - :min-height="30", - :max-height="350", - disabled="" - ) - .row.text-center - .input.col-sm-8(v-if="item.places && item.places.activities") - textarea-autosize.text-small( - placeholder="No Activities", - :value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')", - :min-height="30", - :max-height="350", - disabled="" - ) - .row.text-center - .input.col-sm-8(v-if="item.notes") - textarea-autosize.text-small( - placeholder="No Notes", - :value="item.notes", - :min-height="30", - :max-height="350", - disabled="" - ) diff --git a/template/module/view_step.pug b/template/module/view_step.pug deleted file mode 100644 index 6540d3e..0000000 --- a/template/module/view_step.pug +++ /dev/null @@ -1,19 +0,0 @@ -div(v-for="(e, idx) in journey_data.main", :key="idx") - .bg-dark.text-white(v-if="journey_step_data.section == idx") - .container.section - .aligner.text-center.text-white.text-huge(style="margin-bottom: 5px") - .aligner--itemTop.fleft - a(href="#prev", v-on:click.prevent="prev_step") - i.fas.fa-angle-left - span.container - span.small {{ journey_data.main[idx].title }} {{ journey_step_data.day }} - .text-big.text-gray {{ journey_data.main[idx].step_title[journey_step_data.day] }} - .aligner--itemEnd.fright - a(href="#next", v-on:click.prevent="next_step") - i.fas.fa-angle-right - .row - .col-12.col-sm-12 - include map.pug - .row - .col-12.col-sm-12 - .container diff --git a/template/short.pug b/template/short.pug deleted file mode 100644 index e06e6cb..0000000 --- a/template/short.pug +++ /dev/null @@ -1,10 +0,0 @@ -doctype html -include module/head.pug -main#app(v-cloak) - include module/nav_pub.pug - div( - v-for="(item, idx) in journey_data.main", - :class="idx % 2 === 0 ? 'bg-dark text-white' : ''" - ) - include module/short_sec.pug -include module/foot.pug diff --git a/template/view.pug b/template/view.pug deleted file mode 100644 index a80f3a6..0000000 --- a/template/view.pug +++ /dev/null @@ -1,6 +0,0 @@ -doctype html -include module/head.pug -main#app(v-cloak) - div(v-if="journey_data.main[journey_step_data.section] != undefined") - include module/view_step.pug -include module/foot.pug diff --git a/tsconfig-client.json b/tsconfig-client.json new file mode 100644 index 0000000..04b8e7b --- /dev/null +++ b/tsconfig-client.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "esnext", + "typeRoots": [ + "./node_modules/@types", + "./types/ext" + ], + "lib": [ + "esnext", + "DOM" + ], + "noEmit": true, // Disable emitting output (use esbuild to handle this) + "skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts) + "strict": false, // Disable strict type checks if needed + "moduleResolution": "node", + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 6a54986..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - - "compilerOptions": { - "target": "esnext", - "typeRoots": ["./node_modules/@types", "./types/ext"], - "lib": ["esnext", "DOM"], - "noEmit": true, // Disable emitting output (use esbuild to handle this) - "skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts) - "strict": false, // Disable strict type checks if needed - "moduleResolution": "node", - } -} diff --git a/yarn.lock b/yarn.lock index 3d1e226..098f05a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,19 @@ __metadata: version: 8 cacheKey: 10c0 +"@asamuzakjp/css-color@npm:^2.8.2": + version: 2.8.3 + resolution: "@asamuzakjp/css-color@npm:2.8.3" + dependencies: + "@csstools/css-calc": "npm:^2.1.1" + "@csstools/css-color-parser": "npm:^3.0.7" + "@csstools/css-parser-algorithms": "npm:^3.0.4" + "@csstools/css-tokenizer": "npm:^3.0.3" + lru-cache: "npm:^10.4.3" + checksum: 10c0/e108c92ee5de6d8510c9aaca8375c0aeab730dc9b6d4bd287aea2a0379cfbaa09f0814dcacb3e2ddc5c79d7deedf3f82ec8d1ce0effd4a8fac8415b1fe553798 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-string-parser@npm:7.25.9" @@ -40,6 +53,52 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^5.0.2": + version: 5.0.2 + resolution: "@csstools/color-helpers@npm:5.0.2" + checksum: 10c0/bebaddb28b9eb58b0449edd5d0c0318fa88f3cb079602ee27e88c9118070d666dcc4e09a5aa936aba2fde6ba419922ade07b7b506af97dd7051abd08dfb2959b + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^2.1.1, @csstools/css-calc@npm:^2.1.2": + version: 2.1.2 + resolution: "@csstools/css-calc@npm:2.1.2" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/34ced30553968ef5d5f9e00e3b90b48c47480cf130e282e99d57ec9b09f803aab8bc06325683e72a1518b5e7180a3da8b533f1b462062757c21989a53b482e1a + languageName: node + linkType: hard + +"@csstools/css-color-parser@npm:^3.0.7": + version: 3.0.8 + resolution: "@csstools/css-color-parser@npm:3.0.8" + dependencies: + "@csstools/color-helpers": "npm:^5.0.2" + "@csstools/css-calc": "npm:^2.1.2" + peerDependencies: + "@csstools/css-parser-algorithms": ^3.0.4 + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/90722c5a62ca94e9d578ddf59be604a76400b932bd3d4bd23cb1ae9b7ace8fcf83c06995d2b31f96f4afef24a7cefba79beb11ed7ee4999d7ecfec3869368359 + languageName: node + linkType: hard + +"@csstools/css-parser-algorithms@npm:^3.0.4": + version: 3.0.4 + resolution: "@csstools/css-parser-algorithms@npm:3.0.4" + peerDependencies: + "@csstools/css-tokenizer": ^3.0.3 + checksum: 10c0/d411f07765e14eede17bccc6bd4f90ff303694df09aabfede3fd104b2dfacfd4fe3697cd25ddad14684c850328f3f9420ebfa9f78380892492974db24ae47dbd + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^3.0.3": + version: 3.0.3 + resolution: "@csstools/css-tokenizer@npm:3.0.3" + checksum: 10c0/c31bf410e1244b942e71798e37c54639d040cb59e0121b21712b40015fced2b0fb1ffe588434c5f8923c9cd0017cfc1c1c8f3921abc94c96edf471aac2eba5e5 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/aix-ppc64@npm:0.25.0" @@ -376,6 +435,150 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher-android-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-android-arm64@npm:2.5.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-darwin-arm64@npm:2.5.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-darwin-x64@npm:2.5.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-freebsd-x64@npm:2.5.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.5.1" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm-musl@npm:2.5.1" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.5.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.5.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.5.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.5.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-arm64@npm:2.5.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-ia32@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-ia32@npm:2.5.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.5.1": + version: 2.5.1 + resolution: "@parcel/watcher-win32-x64@npm:2.5.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.4.1": + version: 2.5.1 + resolution: "@parcel/watcher@npm:2.5.1" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.5.1" + "@parcel/watcher-darwin-arm64": "npm:2.5.1" + "@parcel/watcher-darwin-x64": "npm:2.5.1" + "@parcel/watcher-freebsd-x64": "npm:2.5.1" + "@parcel/watcher-linux-arm-glibc": "npm:2.5.1" + "@parcel/watcher-linux-arm-musl": "npm:2.5.1" + "@parcel/watcher-linux-arm64-glibc": "npm:2.5.1" + "@parcel/watcher-linux-arm64-musl": "npm:2.5.1" + "@parcel/watcher-linux-x64-glibc": "npm:2.5.1" + "@parcel/watcher-linux-x64-musl": "npm:2.5.1" + "@parcel/watcher-win32-arm64": "npm:2.5.1" + "@parcel/watcher-win32-ia32": "npm:2.5.1" + "@parcel/watcher-win32-x64": "npm:2.5.1" + detect-libc: "npm:^1.0.3" + is-glob: "npm:^4.0.3" + micromatch: "npm:^4.0.5" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm-musl": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-ia32": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10c0/8f35073d0c0b34a63d4c8d2213482f0ebc6a25de7b2cdd415d19cb929964a793cb285b68d1d50bfb732b070b3c82a2fdb4eb9c250eab709a1cd9d63345455a82 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -383,17 +586,6 @@ __metadata: languageName: node linkType: hard -"@prettier/plugin-pug@npm:^3.0.0": - version: 3.2.1 - resolution: "@prettier/plugin-pug@npm:3.2.1" - dependencies: - pug-lexer: "npm:^5.0.1" - peerDependencies: - prettier: ^3.0.0 - checksum: 10c0/f361496e1669e308e1e74b3de2f524ba92364904142b23cf496bc26531f8cab3ccea965d0cb32b6272656c0415f0dbfe422aa6dc844f56f89cbdbdc815899390 - languageName: node - linkType: hard - "@types/node@npm:^22.13.5": version: 22.13.5 resolution: "@types/node@npm:22.13.5" @@ -551,17 +743,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.9": - version: 1.7.9 - resolution: "axios@npm:1.7.9" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10c0/b7a41e24b59fee5f0f26c1fc844b45b17442832eb3a0fb42dd4f1430eb4abc571fe168e67913e8a1d91c993232bd1d1ab03e20e4d1fee8c6147649b576fc1b0b - languageName: node - linkType: hard - "babel-walk@npm:3.0.0-canary-5": version: 3.0.0-canary-5 resolution: "babel-walk@npm:3.0.0-canary-5" @@ -611,7 +792,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:~3.0.2": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -686,7 +867,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^3.5.2": +"chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.5.2": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -705,6 +886,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^4.0.0": + version: 4.0.3 + resolution: "chokidar@npm:4.0.3" + dependencies: + readdirp: "npm:^4.0.1" + checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -781,6 +971,36 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:1.1.3": + version: 1.1.3 + resolution: "css-tree@npm:1.1.3" + dependencies: + mdn-data: "npm:2.0.14" + source-map: "npm:^0.6.1" + checksum: 10c0/499a507bfa39b8b2128f49736882c0dd636b0cd3370f2c69f4558ec86d269113286b7df469afc955de6a68b0dba00bc533e40022a73698081d600072d5d83c1c + languageName: node + linkType: hard + +"cssstyle@npm:^4.2.1": + version: 4.2.1 + resolution: "cssstyle@npm:4.2.1" + dependencies: + "@asamuzakjp/css-color": "npm:^2.8.2" + rrweb-cssom: "npm:^0.8.0" + checksum: 10c0/02ba8c47c0caaab57acadacb3eb6c0f5f009000f55d61f6563670e07d389b26edefeed497e6c1847fcd2e6bbe0b6974c2d4291f97fa0c6ec6add13a7fa926d84 + languageName: node + linkType: hard + +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.0.0" + checksum: 10c0/1b894d7d41c861f3a4ed2ae9b1c3f0909d4575ada02e36d3d3bc584bdd84278e20709070c79c3b3bff7ac98598cb191eb3e86a89a79ea4ee1ef360e1694f92ad + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4, debug@npm:^4.3.4": version: 4.4.0 resolution: "debug@npm:4.4.0" @@ -793,6 +1013,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.5.0 + resolution: "decimal.js@npm:10.5.0" + checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3 + languageName: node + linkType: hard + "deferred-leveldown@npm:^7.0.0": version: 7.0.0 resolution: "deferred-leveldown@npm:7.0.0" @@ -824,6 +1051,15 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^1.0.3": + version: 1.0.3 + resolution: "detect-libc@npm:1.0.3" + bin: + detect-libc: ./bin/detect-libc.js + checksum: 10c0/4da0deae9f69e13bc37a0902d78bf7169480004b1fed3c19722d56cff578d16f0e11633b7fbf5fb6249181236c72e90024cbd68f0b9558ae06e281f47326d50d + languageName: node + linkType: hard + "doctypes@npm:^1.1.0": version: 1.1.0 resolution: "doctypes@npm:1.1.0" @@ -884,6 +1120,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^4.5.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -933,6 +1176,20 @@ __metadata: languageName: node linkType: hard +"esbuild-plugin-sass@npm:^1.0.1": + version: 1.0.1 + resolution: "esbuild-plugin-sass@npm:1.0.1" + dependencies: + css-tree: "npm:1.1.3" + fs-extra: "npm:10.0.0" + sass: "npm:1.47.0" + tmp: "npm:0.2.1" + peerDependencies: + esbuild: ">=0.11.14" + checksum: 10c0/4a8b0aec2de41a44bfeb5518e5754be6b8a39a8873f4e543bfb40595636db639b5a5f9e2561594c8de7c81e5dddb88b83e73336827cada875203a185b4a1d053 + languageName: node + linkType: hard + "esbuild@npm:^0.25.0": version: 0.25.0 resolution: "esbuild@npm:0.25.0" @@ -1091,7 +1348,7 @@ __metadata: languageName: node linkType: hard -"fastify@npm:^5.0.0": +"fastify@npm:^5.2.1": version: 5.2.1 resolution: "fastify@npm:5.2.1" dependencies: @@ -1143,16 +1400,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/5829165bd112c3c0e82be6c15b1a58fa9dcfaede3b3c54697a82fe4a62dd5ae5e8222956b448d2f98e331525f05d00404aba7d696de9e761ef6e42fdc780244f - languageName: node - linkType: hard - "foreground-child@npm:^3.1.0": version: 3.3.0 resolution: "foreground-child@npm:3.3.0" @@ -1163,7 +1410,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": +"form-data@npm:^4.0.1": version: 4.0.2 resolution: "form-data@npm:4.0.2" dependencies: @@ -1175,6 +1422,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:10.0.0": + version: 10.0.0 + resolution: "fs-extra@npm:10.0.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/85802f3d9e49d197744a8372f0d78d5a1faa3df73f4c5375d6366a4b9f745197d3da1f095841443d50f29a9f81cdc01363eb6d17bef2ba70c268559368211040 + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -1184,6 +1442,13 @@ __metadata: languageName: node linkType: hard +"fs.realpath@npm:^1.0.0": + version: 1.0.0 + resolution: "fs.realpath@npm:1.0.0" + checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + languageName: node + linkType: hard + "fsevents@npm:~2.3.2": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -1279,6 +1544,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:^7.1.3": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.1.1" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10c0/65676153e2b0c9095100fe7f25a778bf45608eeb32c6048cf307f579649bcc30353277b3b898a3792602c65764e5baa4f643714dfbdfd64ea271d210c7a425fe + languageName: node + linkType: hard + "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -1286,7 +1565,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -1325,6 +1604,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: "npm:^3.1.1" + checksum: 10c0/523398055dc61ac9b34718a719cb4aa691e4166f29187e211e1607de63dc25ac7af52ca7c9aead0c4b3c0415ffecb17326396e1202e2e86ff4bca4c0ee4c6140 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -1345,7 +1633,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -1355,7 +1643,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -1365,7 +1653,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -1388,6 +1676,20 @@ __metadata: languageName: node linkType: hard +"immutable@npm:^4.0.0": + version: 4.3.7 + resolution: "immutable@npm:4.3.7" + checksum: 10c0/9b099197081b22f6433003e34929da8ecddbbdc1474cdc8aa3b7669dee4adda349c06143de22def36016d1b6de5322b043eccd7a11db1dad2ca85dad4fff5435 + languageName: node + linkType: hard + +"immutable@npm:^5.0.2": + version: 5.0.3 + resolution: "immutable@npm:5.0.3" + checksum: 10c0/3269827789e1026cd25c2ea97f0b2c19be852ffd49eda1b674b20178f73d84fa8d945ad6f5ac5bc4545c2b4170af9f6e1f77129bc1cae7974a4bf9b04a9cdfb9 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -1395,7 +1697,17 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4": +"inflight@npm:^1.0.4": + version: 1.0.6 + resolution: "inflight@npm:1.0.6" + dependencies: + once: "npm:^1.3.0" + wrappy: "npm:1" + checksum: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 + languageName: node + linkType: hard + +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:^2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -1468,7 +1780,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1": +"is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -1484,6 +1796,13 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + "is-promise@npm:^2.0.0": version: 2.2.2 resolution: "is-promise@npm:2.2.2" @@ -1553,6 +1872,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^26.0.0": + version: 26.0.0 + resolution: "jsdom@npm:26.0.0" + dependencies: + cssstyle: "npm:^4.2.1" + data-urls: "npm:^5.0.0" + decimal.js: "npm:^10.4.3" + form-data: "npm:^4.0.1" + html-encoding-sniffer: "npm:^4.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.6" + is-potential-custom-element-name: "npm:^1.0.1" + nwsapi: "npm:^2.2.16" + parse5: "npm:^7.2.1" + rrweb-cssom: "npm:^0.8.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^5.0.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + whatwg-encoding: "npm:^3.1.1" + whatwg-mimetype: "npm:^4.0.0" + whatwg-url: "npm:^14.1.0" + ws: "npm:^8.18.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc + languageName: node + linkType: hard + "json-schema-ref-resolver@npm:^2.0.0": version: 2.0.1 resolution: "json-schema-ref-resolver@npm:2.0.1" @@ -1569,6 +1922,19 @@ __metadata: languageName: node linkType: hard +"jsonfile@npm:^6.0.1": + version: 6.1.0 + resolution: "jsonfile@npm:6.1.0" + dependencies: + graceful-fs: "npm:^4.1.6" + universalify: "npm:^2.0.0" + dependenciesMeta: + graceful-fs: + optional: true + checksum: 10c0/4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865 + languageName: node + linkType: hard + "jstransformer@npm:1.0.0": version: 1.0.0 resolution: "jstransformer@npm:1.0.0" @@ -1658,7 +2024,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.4.3": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10c0/ebd04fbca961e6c1d6c0af3799adcc966a1babe798f685bb84e6599266599cd95d94630b10262f5424539bc4640107e8a33aa28585374abf561d30d16f4b39fb @@ -1698,6 +2064,23 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.0.14": + version: 2.0.14 + resolution: "mdn-data@npm:2.0.14" + checksum: 10c0/67241f8708c1e665a061d2b042d2d243366e93e5bf1f917693007f6d55111588b952dcbfd3ea9c2d0969fb754aad81b30fdcfdcc24546495fc3b24336b28d4bd + languageName: node + linkType: hard + +"micromatch@npm:^4.0.5": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + "mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -1732,7 +2115,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.1.2": +"minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -1857,6 +2240,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9 + languageName: node + linkType: hard + "node-gyp-build@npm:^4.3.0": version: 4.8.4 resolution: "node-gyp-build@npm:4.8.4" @@ -1926,6 +2318,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.16": + version: 2.2.16 + resolution: "nwsapi@npm:2.2.16" + checksum: 10c0/0aa0637f4d51043d0183d994e08336bae996b03b42984381bf09ebdf3ff4909c018eda6b2a8aba0a08f3ea8303db8a0dad0608b38dc0bff15fd87017286ae21a + languageName: node + linkType: hard + "object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -1940,6 +2339,15 @@ __metadata: languageName: node linkType: hard +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: "npm:1" + checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + languageName: node + linkType: hard + "p-map@npm:^7.0.2": version: 7.0.3 resolution: "p-map@npm:7.0.3" @@ -1954,6 +2362,22 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^7.2.1": + version: 7.2.1 + resolution: "parse5@npm:7.2.1" + dependencies: + entities: "npm:^4.5.0" + checksum: 10c0/829d37a0c709215a887e410a7118d754f8e1afd7edb529db95bc7bbf8045fb0266a7b67801331d8e8d9d073ea75793624ec27ce9ff3b96862c3b9008f4d68e80 + languageName: node + linkType: hard + +"path-is-absolute@npm:^1.0.0": + version: 1.0.1 + resolution: "path-is-absolute@npm:1.0.1" + checksum: 10c0/127da03c82172a2a50099cddbf02510c1791fc2cc5f7713ddb613a56838db1e8168b121a920079d052e0936c23005562059756d653b7c544c53185efe53be078 + languageName: node + linkType: hard + "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -1988,7 +2412,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -2032,13 +2456,6 @@ __metadata: languageName: node linkType: hard -"pretier@npm:^0.0.1": - version: 0.0.1 - resolution: "pretier@npm:0.0.1" - checksum: 10c0/206f5b353c32a9ad0e38243ad2caf7c8859ef43c975514e1c725088e4925d8b1480f73b96ebfd5ceaa1a563a77a3b8c893db7bb3642585bfa0c635f7ee149885 - languageName: node - linkType: hard - "prettier@npm:^3.5.2": version: 3.5.2 resolution: "prettier@npm:3.5.2" @@ -2081,13 +2498,6 @@ __metadata: languageName: node linkType: hard -"proxy-from-env@npm:^1.1.0": - version: 1.1.0 - resolution: "proxy-from-env@npm:1.1.0" - checksum: 10c0/fe7dd8b1bdbbbea18d1459107729c3e4a2243ca870d26d34c2c1bcd3e4425b7bcc5112362df2d93cc7fb9746f6142b5e272fd1cc5c86ddf8580175186f6ad42b - languageName: node - linkType: hard - "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" @@ -2222,6 +2632,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.3": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -2247,6 +2664,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^4.0.1": + version: 4.1.2 + resolution: "readdirp@npm:4.1.2" + checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62 + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -2324,6 +2748,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^3.0.0": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: "npm:^7.1.3" + bin: + rimraf: bin.js + checksum: 10c0/9cb7757acb489bd83757ba1a274ab545eafd75598a9d817e0c3f8b164238dd90eba50d6b848bd4dcc5f3040912e882dc7ba71653e35af660d77b25c381d402e8 + languageName: node + linkType: hard + "rimraf@npm:^5.0.5": version: 5.0.10 resolution: "rimraf@npm:5.0.10" @@ -2335,6 +2770,13 @@ __metadata: languageName: node linkType: hard +"rrweb-cssom@npm:^0.8.0": + version: 0.8.0 + resolution: "rrweb-cssom@npm:0.8.0" + checksum: 10c0/56f2bfd56733adb92c0b56e274c43f864b8dd48784d6fe946ef5ff8d438234015e59ad837fc2ad54714b6421384141c1add4eb569e72054e350d1f8a50b8ac7b + languageName: node + linkType: hard + "safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -2365,6 +2807,45 @@ __metadata: languageName: node linkType: hard +"sass@npm:1.47.0": + version: 1.47.0 + resolution: "sass@npm:1.47.0" + dependencies: + chokidar: "npm:>=3.0.0 <4.0.0" + immutable: "npm:^4.0.0" + source-map-js: "npm:>=0.6.2 <2.0.0" + bin: + sass: sass.js + checksum: 10c0/863afee6aac8529a009d184e5cf8cd79dbd4d4759a49170b2ece5a519911311b630f9630e66db287c4e883d5635c4e8a2a12e0d4579bf484df72680a64dccf56 + languageName: node + linkType: hard + +"sass@npm:^1.85.1": + version: 1.85.1 + resolution: "sass@npm:1.85.1" + dependencies: + "@parcel/watcher": "npm:^2.4.1" + chokidar: "npm:^4.0.0" + immutable: "npm:^5.0.2" + source-map-js: "npm:>=0.6.2 <2.0.0" + dependenciesMeta: + "@parcel/watcher": + optional: true + bin: + sass: sass.js + checksum: 10c0/f843aa1df1dca2f0e9cb2fb247e4939fd514ae4c182cdd1900a0622c0d71b40dfb1c4225f78b78e165a318287ca137ec597695db3e496408bd16a921a2bc2b3f + languageName: node + linkType: hard + +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + "secure-json-parse@npm:^3.0.1": version: 3.0.2 resolution: "secure-json-parse@npm:3.0.2" @@ -2464,6 +2945,20 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:>=0.6.2 <2.0.0": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + +"source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + "split2@npm:^4.0.0": version: 4.2.0 resolution: "split2@npm:4.2.0" @@ -2559,6 +3054,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + "tar@npm:^7.4.3": version: 7.4.3 resolution: "tar@npm:7.4.3" @@ -2582,6 +3084,33 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.78": + version: 6.1.78 + resolution: "tldts-core@npm:6.1.78" + checksum: 10c0/aea5e664da879cd862ccf5df9286531ddf4c34a9ca832480188bf6cd165cd45654f5b0a0f0f5315e16203ebfb87d52f8630b9419e729b3cfe5eff073c398693e + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.78 + resolution: "tldts@npm:6.1.78" + dependencies: + tldts-core: "npm:^6.1.78" + bin: + tldts: bin/cli.js + checksum: 10c0/966f3f5a63405db6abb49b479784baa677510993f21ffbd67571f3d819451d70a603f1246b13f1c309a7573c4d9fbe0241aca6ff6e8399cbe7d2dd70b7ee4052 + languageName: node + linkType: hard + +"tmp@npm:0.2.1": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: "npm:^3.0.0" + checksum: 10c0/67607aa012059c9ce697bee820ee51bc0f39b29a8766def4f92d3f764d67c7cf9205d537d24e0cb1ce9685c40d4c628ead010910118ea18348666b5c46ed9123 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -2621,6 +3150,24 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^5.0.0": + version: 5.1.1 + resolution: "tough-cookie@npm:5.1.1" + dependencies: + tldts: "npm:^6.1.32" + checksum: 10c0/84fe18b7c28ce273c916d95028c00ffff58c285d58e90fbd44eb9380dd1bc21892c675cd1bbd4bfbc95108fe833c406b285844757d41636248bfe264655a6ef8 + languageName: node + linkType: hard + +"tr46@npm:^5.0.0": + version: 5.0.0 + resolution: "tr46@npm:5.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee + languageName: node + linkType: hard + "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" @@ -2635,6 +3182,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.3.0": + version: 7.3.0 + resolution: "undici@npm:7.3.0" + checksum: 10c0/62c5e335725cadb02e19950932c7823fc330cbfd80106e6836daa6db1379aa727510b77de0a4e6f912087b288ded93f7daf4b8c154ad36fd5c9c4b96b26888b8 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -2653,6 +3207,13 @@ __metadata: languageName: node linkType: hard +"universalify@npm:^2.0.0": + version: 2.0.1 + resolution: "universalify@npm:2.0.1" + checksum: 10c0/73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -2674,18 +3235,61 @@ __metadata: "@fastify/leveldb": "npm:^6.0.0" "@fastify/static": "npm:^8.0.0" "@fastify/view": "npm:^10.0.0" - "@prettier/plugin-pug": "npm:^3.0.0" "@types/node": "npm:^22.13.5" - axios: "npm:^1.7.9" esbuild: "npm:^0.25.0" - fastify: "npm:^5.0.0" + esbuild-plugin-sass: "npm:^1.0.1" + fastify: "npm:^5.2.1" + jsdom: "npm:^26.0.0" nodemon: "npm:^3.0.1" - pretier: "npm:^0.0.1" prettier: "npm:^3.5.2" pug: "npm:^3.0.2" + sass: "npm:^1.85.1" + undici: "npm:^7.3.0" languageName: unknown linkType: soft +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + +"webidl-conversions@npm:^7.0.0": + version: 7.0.0 + resolution: "webidl-conversions@npm:7.0.0" + checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 + languageName: node + linkType: hard + +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10c0/273b5f441c2f7fda3368a496c3009edbaa5e43b71b09728f90425e7f487e5cef9eb2b846a31bd760dd8077739c26faf6b5ca43a5f24033172b003b72cf61a93e + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: 10c0/a773cdc8126b514d790bdae7052e8bf242970cebd84af62fb2f35a33411e78e981f6c0ab9ed1fe6ec5071b09d5340ac9178e05b52d35a9c4bcf558ba1b1551df + languageName: node + linkType: hard + +"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0": + version: 14.1.1 + resolution: "whatwg-url@npm:14.1.1" + dependencies: + tr46: "npm:^5.0.0" + webidl-conversions: "npm:^7.0.0" + checksum: 10c0/de1e9cc2f04cb000f232c839d4999384ba41b680ef8a89e7c61c9bc40354ba8593c775d068faaf0819f5866e4d6ca3e7b9b386e2123aa475bfd33da02316f476 + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -2742,6 +3346,42 @@ __metadata: languageName: node linkType: hard +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + languageName: node + linkType: hard + +"ws@npm:^8.18.0": + version: 8.18.1 + resolution: "ws@npm:8.18.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/e498965d6938c63058c4310ffb6967f07d4fa06789d3364829028af380d299fe05762961742971c764973dce3d1f6a2633fe8b2d9410c9b52e534b4b882a99fa + languageName: node + linkType: hard + +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0"