Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
00fc29c6ad | |||
50b6e0104b | |||
18972a0cbb |
10
.dockerignore
Normal file
|
@ -0,0 +1,10 @@
|
|||
.woodpecker
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.git
|
||||
docker-compose.build.yml
|
||||
node_modules/
|
||||
.env.example
|
||||
LICENSE.md
|
||||
docker-compose.yml
|
||||
README.md
|
|
@ -1,4 +1 @@
|
|||
TAG=0.5.0
|
||||
ORIGIN=https://pianello.webdeploy.it
|
||||
PUBLIC_BACKEND_URL=https://ale-dev.teck-developer.com
|
||||
BODY_SIZE_LIMIT=Infinity
|
||||
TAG=0.0.1
|
2
.gitignore
vendored
|
@ -2,4 +2,4 @@ node_modules/
|
|||
dist/
|
||||
service-worker.build.js
|
||||
.env
|
||||
static/images/mockups
|
||||
static/images/mockups
|
|
@ -1,23 +1,18 @@
|
|||
steps:
|
||||
build_and_deploy:
|
||||
image: git.webdeploy.it/webdeploy/alpine
|
||||
secrets: [docker_password, docker_username, runner_private_key]
|
||||
pipeline:
|
||||
build:
|
||||
image: alpine:3.14
|
||||
secrets: [docker_password, docker_username]
|
||||
commands:
|
||||
################### Provisioning
|
||||
# Install deps
|
||||
- apk add docker docker-compose jq
|
||||
# Log into docker registry
|
||||
- echo "$${DOCKER_PASSWORD}" | docker login --password-stdin --username "$${DOCKER_USERNAME}" git.webdeploy.it
|
||||
- echo "TAG=$(jq -r .version ./frontend/package.json)" >> .env
|
||||
- echo "ORIGIN=https://pianello.webdeploy.it" >> .env
|
||||
- echo "PUBLIC_BACKEND_URL=https://ale-dev.teck-developer.com" >> ./frontend/.env
|
||||
- echo "TAG=$(jq -r .version ./package.json)" >> .env
|
||||
# Build image
|
||||
- docker-compose -f docker-compose.build.yml build
|
||||
- docker push git.webdeploy.it/pianello/frontend:latest
|
||||
- docker push git.webdeploy.it/pianello/frontend:$(jq -r .version ./frontend/package.json)
|
||||
#### Deploy
|
||||
- config_ssh.sh "$${RUNNER_PRIVATE_KEY}"
|
||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||
- cat .env | ssh pianello@pianello.webdeploy.it "cat - > .env" # copy env
|
||||
- cat docker-compose.prod.yml | ssh pianello@pianello.webdeploy.it "cat - > docker-compose.yml && docker compose pull && docker compose up -d --remove-orphans"
|
||||
- docker push git.webdeploy.it/pianello/pianello-web-app:latest
|
||||
- docker push git.webdeploy.it/pianello/pianello-web-app:$(jq -r .version ./package.json)
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
when:
|
||||
branch: main
|
||||
branches: main
|
||||
|
|
16
Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
FROM node:18 AS build
|
||||
|
||||
WORKDIR /usr
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . ./
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=build /usr/dist /usr/share/nginx/html
|
||||
#COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["nginx","-g","daemon off;"]
|
|
@ -3,8 +3,6 @@
|
|||
### CI/CD
|
||||
[![status-badge](https://cicd.webdeploy.it/api/badges/pianello/pianello-web-app/status.svg)](https://cicd.webdeploy.it/pianello/pianello-web-app)
|
||||
|
||||
[![status-badge](https://kuma.dashboard.webdeploy.it/api/badge/28/uptime/400)](https://kuma.dashboard.webdeploy.it)
|
||||
|
||||
### Legal
|
||||
|
||||
This project is licensed under the [GNU General Public License v3](./LICENSE.md).
|
||||
|
|
16
build-sw.js
Executable file
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import {injectManifest} from 'workbox-build';
|
||||
|
||||
injectManifest({
|
||||
swSrc: './src/js/service-worker.js',
|
||||
swDest: './dist/service-worker.js',
|
||||
globDirectory: './dist',
|
||||
globPatterns: [
|
||||
'**/*.js',
|
||||
'**/*.css',
|
||||
'**/*.svg',
|
||||
'**/*.ttf',
|
||||
'**/*.png',
|
||||
]
|
||||
});
|
|
@ -1,16 +1,13 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: git.webdeploy.it/pianello/frontend
|
||||
pianello-web-app:
|
||||
image: git.webdeploy.it/pianello/pianello-web-app
|
||||
restart: always
|
||||
build:
|
||||
context: frontend
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
PUBLIC_BACKEND_URL: ${PUBLIC_BACKEND_URL}
|
||||
|
||||
version_tag:
|
||||
extends: frontend
|
||||
image: git.webdeploy.it/pianello/frontend:${TAG}
|
||||
|
||||
extends: pianello-web-app
|
||||
image: git.webdeploy.it/pianello/pianello-web-app:${TAG}
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
version: '3.7'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
image: git.webdeploy.it/pianello/frontend:latest
|
||||
web-app:
|
||||
image: git.webdeploy.it/pianello/pianello-web-app:latest
|
||||
restart: always
|
||||
environment:
|
||||
ORIGIN: "${ORIGIN}"
|
||||
PORT: 8000
|
||||
BODY_SIZE_LIMIT: Infinity
|
||||
container_name: pianello-web-app
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
- 127.0.0.1:8080:80
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -1,30 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
11
frontend/.gitignore
vendored
|
@ -1,11 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.pnp*
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
FROM git.webdeploy.it/webdeploy/yarn AS build
|
||||
|
||||
WORKDIR /usr
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY . ./
|
||||
# RUN npm run check
|
||||
RUN npm run build
|
||||
|
||||
FROM git.webdeploy.it/webdeploy/yarn
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY --from=build /usr/build /app
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["node", "/app"]
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/bash
|
||||
|
||||
echo "function shutdownGracefully() {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdownGracefully);
|
||||
process.on('SIGTERM', shutdownGracefully);" >> ./build/index.js
|
5798
frontend/package-lock.json
generated
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build && ./add-sigint.sh",
|
||||
"add-sigint": "./add-sigint.sh",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.4",
|
||||
"@sveltejs/kit": "^2.5.28",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.44.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^4.0.2",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.7"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sentry/sveltekit": "^8.31.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-gpx": "^2.1.2",
|
||||
"svelte-preprocess": "^6.0.2"
|
||||
}
|
||||
}
|
76
frontend/src/app.d.ts
vendored
|
@ -1,76 +0,0 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
export interface Error {
|
||||
message: string;
|
||||
}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
interface Category {
|
||||
id: 1;
|
||||
name_it: 'Natura';
|
||||
name_en: 'Nature';
|
||||
description_it: 'Giri nella natura';
|
||||
description_en: 'Countryside routes';
|
||||
icon: '';
|
||||
created_at: null;
|
||||
updated_at: null;
|
||||
deleted_at: null;
|
||||
color: string;
|
||||
cover: string;
|
||||
}
|
||||
interface Sport {
|
||||
id: 1;
|
||||
name_it: 'Trekking';
|
||||
name_en: 'Trekking';
|
||||
description_it: 'Percorso escursionistico';
|
||||
description_en: 'Trekking route';
|
||||
icon: '';
|
||||
created_at: '2023-10-31T18:37:30.000000Z';
|
||||
updated_at: null;
|
||||
deleted_at: null;
|
||||
}
|
||||
interface SportDetails {
|
||||
id: 1;
|
||||
route_id: 1;
|
||||
sport_id: 1;
|
||||
short_description_it: 'Percorso escursionistico intermedio. Buon allenamento richiesto. Sentieri facilmente percorribili. Adatto a ogni livello di abilit\u00e0. ';
|
||||
short_description_en: 'Intermediate hiking route. Good training required. Easily accessible paths. Suitable for all skill levels.';
|
||||
gpx_path: '';
|
||||
distance: 16800;
|
||||
duration: 288;
|
||||
elevation_gain: 439;
|
||||
elevation_loss: null;
|
||||
altitude_max: 620;
|
||||
altitude_min: 180;
|
||||
difficulty_it: 'Facile';
|
||||
difficulty_en: 'Easy';
|
||||
route_type_it: 'Percorso ad anello';
|
||||
route_type_en: 'Ring route';
|
||||
created_at: '2023-11-02T10:57:41.000000Z';
|
||||
updated_at: null;
|
||||
deleted_at: null;
|
||||
sport: Sport;
|
||||
}
|
||||
interface Route {
|
||||
id: number;
|
||||
cover: string;
|
||||
name_it: string;
|
||||
name_en: string;
|
||||
/* Sometimes from API comes 'title' instead of 'name' :c */
|
||||
title_it: string;
|
||||
title_en: string;
|
||||
description_it: string;
|
||||
description_en: string;
|
||||
route_category_id: number;
|
||||
created_at: '2023-11-02T10:50:07.000000Z';
|
||||
updated_at: null;
|
||||
deleted_at: null;
|
||||
route_sport_details: Array<SportDetails>;
|
||||
elevation_gain?: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
export { Route, Category, Sport, SportDetails };
|
|
@ -1,48 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<style>
|
||||
:root {
|
||||
--footer-height: 108px;
|
||||
--accent-color: #213c8b;
|
||||
--pianello-red: #de0e1b;
|
||||
--pianello-yellow: #f6ae04;
|
||||
--pianello-blue: #213c8b;
|
||||
--card-background-color: #f9f4f1;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
background-color: #fff;
|
||||
font-family: 'Roboto-Regular';
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto-Regular';
|
||||
src: url('/fonts/roboto/Roboto-Regular.ttf') format('TrueType');
|
||||
}
|
||||
</style>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,41 +0,0 @@
|
|||
:root {
|
||||
--footer-height: calc(68px + 20px + 20px);
|
||||
--accent-color: #213c8b;
|
||||
--pianello-red: #de0e1b;
|
||||
--pianello-yellow: #f6ae04;
|
||||
--pianello-blue: #213c8b;
|
||||
--card-background-color: #f9f4f1;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
background-color: #fff;
|
||||
font-family: 'Roboto-Regular';
|
||||
display: grid;
|
||||
}
|
||||
|
||||
header,
|
||||
main,
|
||||
footer {
|
||||
background: white;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto-Regular';
|
||||
src: url('/fonts/roboto/Roboto-Regular.ttf') format('TrueType');
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
.header {
|
||||
padding: 15px;
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
margin: 0 auto;
|
||||
padding-left: 25px;
|
||||
padding-right: 25px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 70px;
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image: url(/splash.webp);
|
||||
background-size: cover;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.wrapper {
|
||||
display: flex;
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#status {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
p {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<p id="status">Code %sveltekit.status%</p>
|
||||
<p id="message">%sveltekit.error.message%</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
export async function handleError({ error, message }) {
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
message
|
||||
};
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
export async function handle({ event, resolve }) {
|
||||
const response = await resolve(event);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function handleFetch({ request, fetch }) {
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
export async function handleError({ error, message }) {
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
message
|
||||
};
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<div id="container-of-container">
|
||||
<div id="container">
|
||||
<a href="/">
|
||||
<img src="/images/home-icon.png" alt="home" />
|
||||
</a>
|
||||
<a href="/">
|
||||
<img src="/images/routes-icon.png" alt="possible routes" />
|
||||
</a>
|
||||
<a href="/settings">
|
||||
<img src="/images/settings-icon.png" alt="settings" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#container-of-container {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 0;
|
||||
display: grid;
|
||||
height: 10dvh;
|
||||
}
|
||||
#container {
|
||||
display: flex;
|
||||
height: 10vh;
|
||||
justify-content: center;
|
||||
width: 90%;
|
||||
border-radius: 90px;
|
||||
box-shadow: 0 0 50px #ccc;
|
||||
background-color: var(--card-background-color);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#container > a {
|
||||
display: block;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||
<script lang="ts">
|
||||
import '../../css/header.css';
|
||||
|
||||
export const title: string = 'Naturalistici';
|
||||
|
||||
const goBack = () => {
|
||||
history.back();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a on:click={goBack}>
|
||||
<img class="back" src="/images/black-back-arrow.png" alt="" />
|
||||
</a>
|
||||
|
||||
<img src="/images/app-bar-logo.png" alt="" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
}
|
||||
div a {
|
||||
cursor: pointer;
|
||||
}
|
||||
div a img {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
}
|
||||
.back {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,45 +0,0 @@
|
|||
<script lang="ts">
|
||||
import '../../css/header.css';
|
||||
|
||||
export let title: string = 'Naturalistici';
|
||||
|
||||
const goBack = () => {
|
||||
history.back();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<a on:click={goBack}>
|
||||
<img class="back" src="/images/white-back-arrow.png" alt="" />
|
||||
</a>
|
||||
<p>{title}</p>
|
||||
<img src="/images/app-bar-logo.png" alt="" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
color: white;
|
||||
background-color: #de0e1b;
|
||||
}
|
||||
div p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
div a {
|
||||
cursor: pointer;
|
||||
}
|
||||
div a img {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
}
|
||||
.back {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
export let name: string;
|
||||
export let id: number;
|
||||
export let color: string;
|
||||
export const path = `/paths/${id}`;
|
||||
export let src: string;
|
||||
|
||||
let image = '/archi.png';
|
||||
switch (id) {
|
||||
case 1:
|
||||
image = '/archi.png';
|
||||
break;
|
||||
case 2:
|
||||
image = '/montagne.png';
|
||||
break;
|
||||
case 3:
|
||||
image = '/bibbito.png';
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
|
||||
<a href={path} class="route-card">
|
||||
<div class="route-card-left" style="background-color: {color}">
|
||||
<img src={image} alt="logo" />
|
||||
</div>
|
||||
<div class="route-card-center">
|
||||
<div class="name">
|
||||
<div class="bold">{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="route-card-right" style:background-image="url({PUBLIC_BACKEND_URL}{src})" />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-template-columns: 20% minmax(1px, 1fr) 20%;
|
||||
max-width: 100%;
|
||||
width: calc(100% - 20px);
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
font-size: 24px;
|
||||
}
|
||||
.route-card-right {
|
||||
border-top-right-radius: 45px;
|
||||
border-bottom-right-radius: 45px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.route-card-left {
|
||||
border-top-left-radius: 45px;
|
||||
border-bottom-left-radius: 45px;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.route-card-left img {
|
||||
width: 100%;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.route-card-center {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -1,73 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
export let route: App.Route;
|
||||
</script>
|
||||
|
||||
<a href="/routes/{route.id}">
|
||||
<div id="image" style:background-image="url({PUBLIC_BACKEND_URL}{route.cover})" />
|
||||
<div id="path-holder">
|
||||
<p class="path-name">{route.title_it}</p>
|
||||
</div>
|
||||
<div id="duration-holder">
|
||||
<div style="font-size: 15px;">Dislivello</div>
|
||||
<p id="duration">{route.elevation_gain}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
display: block;
|
||||
box-shadow: 0 0 50px #ccc;
|
||||
border-radius: 5px;
|
||||
width: calc((100% - 50px));
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#image {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
object-position: center;
|
||||
background-position: center;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
#duration {
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
#duration-holder {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
#path-holder {
|
||||
padding: 10px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
#path-holder p {
|
||||
margin: 0px;
|
||||
margin-top: 2px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
line-height: 1em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.path-name {
|
||||
view-transition-name: title;
|
||||
max-height: 2em;
|
||||
}
|
||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
let visible = dev ? false : true;
|
||||
let time = 2000;
|
||||
|
||||
onMount(() => {
|
||||
if (dev) return;
|
||||
|
||||
setTimeout(() => {
|
||||
visible = false;
|
||||
}, time);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<img class="background" transition:fade src="/splash.webp" alt="splash" />
|
||||
<div class="wrapper">
|
||||
<img transition:fade class="logo" src="/images/splash-logo.png" alt="splash-logo" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
}
|
||||
</style>
|
|
@ -1,97 +0,0 @@
|
|||
<script lang="ts">
|
||||
import InfoTabTrekking from './tabs/InfoTabTrekking.svelte';
|
||||
import InfoTabBike from './tabs/InfoTabBike.svelte';
|
||||
import DescTab from './tabs/DescTab.svelte';
|
||||
import MapTab from './tabs/MapTab.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
export let route: App.Route;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let ref: HTMLDivElement;
|
||||
let component = InfoTabTrekking;
|
||||
|
||||
onMount(() => {
|
||||
console.log(route);
|
||||
});
|
||||
const dispatchClick = (c: InfoTabTrekking | InfoTabBike | DescTab | MapTab) => {
|
||||
component = c;
|
||||
dispatch('tab-click', { component });
|
||||
};
|
||||
</script>
|
||||
|
||||
<div id="tabs">
|
||||
<button
|
||||
class:active={component === InfoTabTrekking}
|
||||
on:click={() => dispatchClick(InfoTabTrekking)}
|
||||
id="info-trekking"
|
||||
>
|
||||
<img src="/trekking.svg" alt="trekking" />
|
||||
</button>
|
||||
<button
|
||||
class:active={component === InfoTabBike}
|
||||
on:click={() => dispatchClick(InfoTabBike)}
|
||||
id="info-bike"
|
||||
>
|
||||
<img src="/bike.svg" alt="bike" />
|
||||
</button>
|
||||
<button class:active={component === DescTab} on:click={() => dispatchClick(DescTab)} id="desc">
|
||||
Descrizione</button
|
||||
>
|
||||
<button class:active={component === MapTab} on:click={() => dispatchClick(MapTab)} id="map">
|
||||
Mappa</button
|
||||
>
|
||||
</div>
|
||||
<div id="container">
|
||||
<div class="tab" bind:this={ref} role="tab">
|
||||
<svelte:component this={component} {route} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#container {
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
}
|
||||
#tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: grey;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#tabs button {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: black !important;
|
||||
font-weight: bold !important;
|
||||
border-bottom: 1px solid black !important;
|
||||
}
|
||||
|
||||
#info-bike,
|
||||
#info-trekking {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,45 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
export let message = "L'applicazione è pronta per funzionare offline!";
|
||||
|
||||
let show = false;
|
||||
let nodeRef: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
show = true;
|
||||
setTimeout(() => {
|
||||
show = false;
|
||||
setTimeout(() => {
|
||||
nodeRef?.parentNode?.removeChild(nodeRef);
|
||||
}, 5000);
|
||||
}, 5000);
|
||||
}, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={nodeRef} id="toast" class:show>
|
||||
<img src="/icons/checkmark.svg" alt="check" />
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.show {
|
||||
transform: translateY(0vh) !important;
|
||||
}
|
||||
#toast {
|
||||
transform: translateY(10vh);
|
||||
transition: transform ease 0.4s;
|
||||
position: fixed;
|
||||
height: 10vh;
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
background-color: #f6ae04;
|
||||
color: black;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,15 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let route: App.Route;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{route.description_it}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let route: App.Route;
|
||||
</script>
|
||||
|
||||
<div id="grid">
|
||||
<div>
|
||||
<p>Distanza</p>
|
||||
<p>{route.route_sport_details[1].distance} m</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Dislivello Positivo</p>
|
||||
<p>{route.route_sport_details[1].elevation_gain}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Velocità Media</p>
|
||||
<p>N/A</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Difficolta</p>
|
||||
<p>{route.route_sport_details[1].difficulty_it}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Dislivello Negativo</p>
|
||||
<p>{route.route_sport_details[1].elevation_loss}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Durata</p>
|
||||
<p>{route.route_sport_details[1].duration}'</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Altitudine max</p>
|
||||
<p>{route.route_sport_details[1].altitude_max}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Altitudine min</p>
|
||||
<p>{route.route_sport_details[1].altitude_min}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Tipo Percorso</p>
|
||||
<p>{route.route_sport_details[1].route_type_it}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#grid {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 33%);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 100%;
|
||||
background: white;
|
||||
place-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#grid div {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
#grid div > p {
|
||||
padding: 10px;
|
||||
margin: 0px;
|
||||
}
|
||||
#grid div p:nth-child(2) {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
|
@ -1,77 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let route: App.Route;
|
||||
</script>
|
||||
|
||||
<div id="grid">
|
||||
<div>
|
||||
<p>Distanza</p>
|
||||
<p>{route.route_sport_details[0].distance} m</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Dislivello Positivo</p>
|
||||
<p>{route.route_sport_details[0].elevation_gain}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Velocità Media</p>
|
||||
<p>N/A</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Difficolta</p>
|
||||
<p>{route.route_sport_details[0].difficulty_it}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Dislivello Negativo</p>
|
||||
<p>{route.route_sport_details[0].elevation_loss}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Durata</p>
|
||||
<p>{route.route_sport_details[0].duration}'</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Altitudine max</p>
|
||||
<p>{route.route_sport_details[0].altitude_max}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Altitudine min</p>
|
||||
<p>{route.route_sport_details[0].altitude_min}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Tipo Percorso</p>
|
||||
<p>{route.route_sport_details[0].route_type_it}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#grid {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 33%);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 100%;
|
||||
background: white;
|
||||
place-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#grid div {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
#grid div > p {
|
||||
padding: 10px;
|
||||
margin: 0px;
|
||||
}
|
||||
#grid div p:nth-child(2) {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
|
@ -1,132 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
const mapMarkerIcon = new URL('/icons/map.png', import.meta.url).href;
|
||||
|
||||
export let route;
|
||||
const pianelloCoordinates = [43.14, 12.53];
|
||||
let mapElement: string | HTMLElement;
|
||||
let leaflet;
|
||||
let leafletGPX;
|
||||
let map;
|
||||
let layerGroup;
|
||||
let latitude;
|
||||
let longitude;
|
||||
let accuracy;
|
||||
let watchPositionId: number;
|
||||
const errorMessage = 'Geolocation not available';
|
||||
const attribution =
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
const openStreetMapTile = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
|
||||
let errored = false;
|
||||
|
||||
const watchPosition = async () => {
|
||||
if (!('geolocation' in navigator)) {
|
||||
errored = true;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const options = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0
|
||||
};
|
||||
|
||||
function success(pos: GeolocationPosition) {
|
||||
const crd = pos.coords;
|
||||
|
||||
let string = '';
|
||||
|
||||
latitude = crd.latitude;
|
||||
longitude = crd.longitude;
|
||||
accuracy = crd.accuracy;
|
||||
|
||||
const icon = leaflet.icon({
|
||||
iconUrl: mapMarkerIcon,
|
||||
iconSize: [75, 75] // size of the icon
|
||||
});
|
||||
userMarker = leaflet.marker([latitude, longitude], { icon }).addTo(map);
|
||||
|
||||
// move the map to have the location in its center
|
||||
map.panTo(userMarker.getLatLng());
|
||||
|
||||
string += 'Your current position is:';
|
||||
string += `Latitude : ${crd.latitude}`;
|
||||
string += `Longitude: ${crd.longitude}`;
|
||||
string += `More or less ${crd.accuracy} meters.`;
|
||||
}
|
||||
|
||||
function error(err) {
|
||||
console.warn(`ERROR(${err.code}): ${err.message}`);
|
||||
}
|
||||
|
||||
watchPositionId = navigator.geolocation.watchPosition(success, error, options);
|
||||
};
|
||||
|
||||
const renderMap = () => {
|
||||
layerGroup = leaflet.layerGroup();
|
||||
|
||||
// Startup Map
|
||||
|
||||
map = leaflet.map(mapElement, {
|
||||
//dragging: leaflet.Browser.mobile,
|
||||
//tap: leaflet.Browser.mobile,
|
||||
});
|
||||
|
||||
map.setView(pianelloCoordinates, 13);
|
||||
|
||||
leaflet.tileLayer(openStreetMapTile, { attribution }).addTo(map);
|
||||
};
|
||||
|
||||
const renderGPX = () => {
|
||||
const gpx = '/gpx/tidone.gpx'; // URL to your GPX file or the GPX itself
|
||||
new leafletGPX(gpx, { async: true })
|
||||
.on('loaded', function (e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
})
|
||||
.addTo(map);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await watchPosition();
|
||||
leaflet = await import('leaflet');
|
||||
const { GPX } = await import('leaflet-gpx');
|
||||
leafletGPX = GPX;
|
||||
|
||||
renderMap();
|
||||
renderGPX();
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
if (watchPositionId) {
|
||||
navigator.geolocation.clearWatch(watchPositionId);
|
||||
}
|
||||
if (map) {
|
||||
console.log('Unloading Leaflet map.');
|
||||
map.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !errored}
|
||||
<div id="map" in:fade bind:this={mapElement} />
|
||||
{:else}
|
||||
{errorMessage}
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +0,0 @@
|
|||
let latitude;
|
||||
let longitude;
|
||||
|
||||
onmessage = () => {};
|
||||
|
||||
export {};
|
|
@ -1,93 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"sequence_number": 2,
|
||||
"name_it": "Cimitero di Pianello Val Tidone",
|
||||
"name_en": "Pianello Val Tidone Cemetery",
|
||||
"description_it": "Il terrazzo fluviale alla confluenza tra Tidone e Chiarone \u00e8 stato fin dall\u2019antichit\u00e0 un luogo privilegiato per l\u2019insediamento umano. \r\n\r\nI materiali conservati presso il Museo Archeologico della Val Tidone evidenziano una continuit\u00e0 di vita dal II sec. a.C. al IV sec. d.C. \r\n\r\nIn particolare, in et\u00e0 romana \u00e8 testimoniata la presenza di un abitato di medie dimensioni, sulle cui strutture, cadute ormai in disuso, si impianta una necropoli altomedievale.",
|
||||
"description_en": "The river terrace at the confluence between Tidone and Chiarone has been a privileged place for human settlement since ancient times.\r\n\r\nThe materials preserved at the Archaeological Museum of Val Tidone highlight a continuity of life from the 2nd century. B.C. to the 4th century. A.D.\r\n\r\nIn particular, in the Roman age there is evidence of the presence of a medium-sized settlement, on whose structures, which had now fallen into disuse, an early medieval necropolis was established.",
|
||||
"latitude": 44.948232,
|
||||
"longitude": 9.446436,
|
||||
"created_at": "2023-11-02T11:10:48.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"sequence_number": 3,
|
||||
"name_it": "Museo Archeologico della Val Tidone ",
|
||||
"name_en": "Archaeological Museum of Val Tidone",
|
||||
"description_it": "Il Museo Archeologico della Val Tidone accoglie e valorizza reperti provenienti dall\u2019intera vallata, raccontando la Storia della presenza umana e le trasformazioni del territorio dalla Preistoria al Medioevo. Il Museo \u00e8 ospitato nei sotterranei della Rocca Dal Verme, ricostruita dopo il passaggio del Barbarossa nel 1164. Prende il nome dal condottiero veneto Jacopo dal Verme, che qui fu infeudato dai Visconti di Milano, creando il cosiddetto Stato Vermesco. Nei sotterranei \u00e8 esposta anche un\u2019opera dell\u2019artista locale Paolo Vincenzo Novara, originario di Borgonovo V.T., ma che visse e lavor\u00f2 sempre a Pianello V.T. Si tratta di una veduta del borgo pianellese e delle colline circostanti.",
|
||||
"description_en": "The Archaeological Museum of Val Tidone welcomes and valorises finds from the entire valley, telling the history of human presence and the transformations of the territory from Prehistory to the Middle Ages. The Museum is housed in the basement of the Rocca Dal Verme, rebuilt after the passage of Barbarossa in 1164. It takes its name from the Venetian leader Jacopo dal Verme, who was enfeoffed here by the Visconti of Milan, creating the so-called Vermesco State. In the basement there is also a work by the local artist Paolo Vincenzo Novara, originally from Borgonovo V.T., but who always lived and worked in Pianello V.T. This is a view of the Pianella village and the surrounding hills.",
|
||||
"latitude": 44.948232,
|
||||
"longitude": 9.40559,
|
||||
"created_at": "2023-11-02T11:10:48.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"sequence_number": 1,
|
||||
"name_it": "Arcello",
|
||||
"name_en": "Arcello",
|
||||
"description_it": "Qui sorgeva una villa romana con ambienti decorati. A fine Ottocento fu rinvenuta una stele funeraria intitolata a Caio Birrivs Mascvlvs, ora a Piacenza. Citato in un documento dell\u2019844, nel 1089 c\u2019era sicuramente un castello, poi distrutto dal Barbarossa e dal Pallavicino: rimangono il basamento della torre e le cantine dell\u2019attuale canonica. Del convento dei Carmelitani, dismesso nel 1652, rimane una torre. Il pittoresco borgo si sviluppa su una terrazza naturale prospiciente la Media Val Tidone e deve la sua importanza per essere stato feudo della nobile famiglia degli Arcelli dal 11 al 14 sec.",
|
||||
"description_en": "Here stood a Roman villa with decorated rooms. At the end of the 19th century, a funerary stele dedicated to Caio Birrivs Mascvlvs was found, now in Piacenza. Mentioned in a document from 844, in 1089 there was certainly a castle, later destroyed by Barbarossa and Pallavicino: the base of the tower and the cellars of the current rectory remain. Of the Carmelite convent, decommissioned in 1652, only one tower remains. The picturesque village develops on a natural terrace overlooking the Middle Val Tidone and owes its importance to having been a fiefdom of the noble Arcelli family from the 11th to the 14th century.",
|
||||
"latitude": 44.952267,
|
||||
"longitude": 9.446436,
|
||||
"created_at": "2023-11-02T11:20:30.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"sequence_number": 4,
|
||||
"name_it": "Case Rebuffi",
|
||||
"name_en": "Case Rebuffi",
|
||||
"description_it": "Numerosi rinvenimenti archeologici del secolo scorso confermano che almeno in et\u00e0 romana qui doveva passare una strada: ormai perduti sono i reperti della sepoltura rinvenuta nel 1928. \r\n\r\nUn tale Rebuffum da Peccoraria compare nel Registrum Magnum di Piacenza quando nel 1187 riceve l\u2019investitura feudale di alcuni terreni: il nome del luogo potrebbe riferirsi a questo antenato. \r\n\r\nIl piccolo oratorio dedicato a S. Andrea di Avellino, ora privato, \u00e8 del XVIII secolo.",
|
||||
"description_en": "Numerous archaeological discoveries from the last century confirm that at least in Roman times a road must have passed here: the remains of the burial found in 1928 are now lost.\r\n\r\nSuch a Rebuffum from Peccoraria appears in the Registrum Magnum of Piacenza when in 1187 he received the feudal investiture of some land: the name of the place could refer to this ancestor.\r\n\r\nThe small oratory dedicated to S. Andrea di Avellino, now private, dates back to the 18th century.",
|
||||
"latitude": 44.937276,
|
||||
"longitude": 9.384442,
|
||||
"created_at": "2023-11-02T11:22:46.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"sequence_number": 5,
|
||||
"name_it": "Casanova",
|
||||
"name_en": "Casanova",
|
||||
"description_it": "Borgo rurale sul versante sinistro della vallata e antico possedimento dei Dal Verme. Rinvenimenti archeologici testimoniano la presenza di un insediamento di et\u00e0 romana. Il borgo \u00e8 citato in documenti del X secolo come parte di un beneficio concesso dal monastero di S. Colombano di Bobbio. Particolare \u00e8 la presenza di due chiese: la prima, ormai in rovina, \u00e8 di origine medievale, e mostra ancora il campanile dotato di megafono d\u2019allarme. La seconda, dedicata a Santa\u202fMaria Assunta, fu edificata tra il 1733 e il 1749 per volont\u00e0 del conte Federico Dal Verme. L'edificio, a navata unica, \u00e8 caratterizzato da una facciata a capanna e da un ampio sagrato. L\u2019interno custodisce, sull'altare maggiore, un quadro raffigurante l'Assunzione\u202fdi autore ignoto.",
|
||||
"description_en": "Rural village on the left side of the valley and ancient possession of the Dal Verme family. Archaeological finds testify to the presence of a Roman settlement. The village is mentioned in documents from the 10th century as part of a benefit granted by the monastery of S. Colombano di Bobbio. The presence of two churches is particular: the first, now in ruins, is of medieval origin, and still shows the bell tower equipped with an alarm megaphone. The second, dedicated to Santa Maria Assunta, was built between 1733 and 1749 by order of Count Federico Dal Verme. The building, with a single nave, is characterized by a gabled fa\u00e7ade and a large churchyard. The interior houses, on the main altar, a painting depicting the Assumption by an unknown artist.",
|
||||
"latitude": 44.927274,
|
||||
"longitude": 9.377744,
|
||||
"created_at": "2023-11-02T11:23:28.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"sequence_number": 6,
|
||||
"name_it": "Montemartino",
|
||||
"name_en": "Montemartino",
|
||||
"description_it": "Il nome del luogo, che ricorda un Santo legato ai longobardi, viene citato in un documento scritto nel X secolo nel Monastero di Bobbio. Nel XIV secolo qui esisteva un castello di propriet\u00e0 dei Da Fontana, le cui strutture sono visibili nel borgo, e la prima chiesa dedicata a S. Bartolomeo, poi rimaneggiata all\u2019inizio del XX secolo.",
|
||||
"description_en": "The name of the place, which recalls a saint linked to the Lombards, is mentioned in a document written in the 10th century in the Monastery of Bobbio. In the 14th century there was a castle here owned by the Da Fontana family, whose structures are visible in the village, and the first church dedicated to S. Bartolomeo, then remodeled at the beginning of the 20th century.",
|
||||
"latitude": 44.904475,
|
||||
"longitude": 9.353009,
|
||||
"created_at": "2023-11-02T11:23:28.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"sequence_number": 7,
|
||||
"name_it": "Vallerenzo",
|
||||
"name_en": "Vallerenzo",
|
||||
"description_it": "Il nome del luogo sembra collegarsi al romano Valerius, nome di famiglia romana presente anche nella stele di Valeria Nardis, conservata al Museo Archeologico della Val Tidone, e sulla Tabula Alimentaria di Veleia. In un documento del 1033 compare un luogo detto Valarinci o Valerinci, mentre nel Registrum Magnum di Piacenza un atto del 1219 \u00e8 redatto proprio in Vallarencio. \r\n\r\nL\u2019oratorio del XVIII secolo, recentemente restaurato, \u00e8 dedicato alla B.V. della Misericordia e S. Lodovico.",
|
||||
"description_en": "The name of the place seems to be connected to the Roman Valerius, a Roman family name also present in the stele of Valeria Nardis, preserved in the Archaeological Museum of Val Tidone, and on the Tabula Alimentaria of Veleia. In a document from 1033 a place called Valarinci or Valerinci appears, while in the Registrum Magnum of Piacenza an act from 1219 is drawn up precisely in Vallarencio.\r\n\r\nThe recently restored 18th century oratory is dedicated to the B.V. della Misericordia and S. Lodovico.",
|
||||
"latitude": 44.891136,
|
||||
"longitude": 9.372671,
|
||||
"created_at": "2023-11-02T11:27:17.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
}
|
||||
]
|
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
"id": 1,
|
||||
"name_it": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"name_en": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"description_it": "Il percorso pone l\u2019attenzione su diversi borghi situati lungo il corso del Tidone. L\u2019insediamento umano in Val Tidone \u00e8 testimoniato fin dalle epoche pi\u00f9 remote, quando l\u2019uomo, in base alle necessit\u00e0 delle varie epoche, sceglie terrazzi fluviali o luoghi pi\u00f9 riparati per impiantare i propri siti abitati. \r\n\r\nL\u2019itinerario partendo da Arcello, passando per Pianello V.T., e risalendo il corso del torrente fino a Vallerenzo, incontra numerosi borghi, dove rinvenimenti archeologici testimoniano la presenza romana e lo sfruttamento agricolo dell\u2019area. In epoca successiva, la costruzione di chiese, castelli e monasteri fa comprendere l\u2019importanza strategica del territorio durante tutto il Medioevo, nelle lotte per il potere e come snodo degli itinerari di commercio e di pellegrinaggio.",
|
||||
"description_en": "The route focuses on various villages located along the Tidone. Human settlement in Val Tidone has been witnessed since the most remote times, when man, based on the needs of the various eras, chose river terraces or more sheltered places to establish his own inhabited sites.\r\n\r\nThe itinerary starting from Arcello, passing through Pianello V.T., and going up the course of the stream to Vallerenzo, encounters numerous villages, where archaeological finds testify to the Roman presence and the agricultural exploitation of the area. In the subsequent era, the construction of churches, castles and monasteries makes us understand the strategic importance of the territory throughout the Middle Ages, in the struggles for power and as a hub for trade and pilgrimage itineraries.",
|
||||
"route_category_id": 2,
|
||||
"created_at": "2023-11-02T10:50:07.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null,
|
||||
"route_sport_details": [
|
||||
{
|
||||
"id": 1,
|
||||
"route_id": 1,
|
||||
"sport_id": 1,
|
||||
"short_description_it": "Percorso escursionistico intermedio. Buon allenamento richiesto. Sentieri facilmente percorribili. Adatto a ogni livello di abilit\u00e0. ",
|
||||
"short_description_en": "Intermediate hiking route. Good training required. Easily accessible paths. Suitable for all skill levels.",
|
||||
"gpx_path": "",
|
||||
"distance": 16800,
|
||||
"duration": 288,
|
||||
"elevation_gain": 439,
|
||||
"elevation_loss": null,
|
||||
"altitude_max": 620,
|
||||
"altitude_min": 180,
|
||||
"difficulty_it": "Facile",
|
||||
"difficulty_en": "Easy",
|
||||
"route_type_it": "Percorso ad anello",
|
||||
"route_type_en": "Ring route",
|
||||
"created_at": "2023-11-02T10:57:41.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null,
|
||||
"sport": {
|
||||
"id": 1,
|
||||
"name_it": "Trekking",
|
||||
"name_en": "Trekking",
|
||||
"description_it": "Percorso escursionistico",
|
||||
"description_en": "Trekking route",
|
||||
"icon": "",
|
||||
"created_at": "2023-10-31T18:37:30.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"route_id": 1,
|
||||
"sport_id": 2,
|
||||
"short_description_it": "Giro in bici per esperti. Ottimo allenamento richiesto. Superfici perlopi\u00f9 asfaltate. Adatto a ogni livello di abilit\u00e0. ",
|
||||
"short_description_en": "Bike ride for experts. Excellent training required. Mostly asphalted surfaces. Suitable for all skill levels.",
|
||||
"gpx_path": "-",
|
||||
"distance": 16800,
|
||||
"duration": 84,
|
||||
"elevation_gain": 333,
|
||||
"elevation_loss": null,
|
||||
"altitude_max": 51,
|
||||
"altitude_min": 181,
|
||||
"difficulty_it": "Facile",
|
||||
"difficulty_en": "Easy",
|
||||
"route_type_it": "Percorso andata/ritorno",
|
||||
"route_type_en": "Round trip route",
|
||||
"created_at": "2023-11-02T11:01:55.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null,
|
||||
"sport": {
|
||||
"id": 2,
|
||||
"name_it": "Cicloturismo",
|
||||
"name_en": "Cycle tourism",
|
||||
"description_it": "Percorso da fare in bicicletta",
|
||||
"description_en": "Cycling route",
|
||||
"icon": "",
|
||||
"created_at": "2023-10-31T18:37:30.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"pictures": []
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title_it": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"title_en": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"description_it": "Il percorso pone l\u2019attenzione su diversi borghi situati lungo il corso del Tidone. L\u2019insediamento umano in Val Tidone \u00e8 testimoniato fin dalle epoche pi\u00f9 remote, quando l\u2019uomo, in base alle necessit\u00e0 delle varie epoche, sceglie terrazzi fluviali o luoghi pi\u00f9 riparati per impiantare i propri siti abitati. \r\n\r\nL\u2019itinerario partendo da Arcello, passando per Pianello V.T., e risalendo il corso del torrente fino a Vallerenzo, incontra numerosi borghi, dove rinvenimenti archeologici testimoniano la presenza romana e lo sfruttamento agricolo dell\u2019area. In epoca successiva, la costruzione di chiese, castelli e monasteri fa comprendere l\u2019importanza strategica del territorio durante tutto il Medioevo, nelle lotte per il potere e come snodo degli itinerari di commercio e di pellegrinaggio.",
|
||||
"description_en": "The route focuses on various villages located along the Tidone. Human settlement in Val Tidone has been witnessed since the most remote times, when man, based on the needs of the various eras, chose river terraces or more sheltered places to establish his own inhabited sites.\r\n\r\nThe itinerary starting from Arcello, passing through Pianello V.T., and going up the course of the stream to Vallerenzo, encounters numerous villages, where archaeological finds testify to the Roman presence and the agricultural exploitation of the area. In the subsequent era, the construction of churches, castles and monasteries makes us understand the strategic importance of the territory throughout the Middle Ages, in the struggles for power and as a hub for trade and pilgrimage itineraries.",
|
||||
"length": 33600,
|
||||
"elevation_gain": 772,
|
||||
"sports": [
|
||||
{ "id": 1, "name_it": "Trekking", "name_en": "Trekking", "duration": 288 },
|
||||
{ "id": 2, "name_it": "Cicloturismo", "name_en": "Cycle tourism", "duration": 84 }
|
||||
],
|
||||
"cover": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title_it": "GLI ARTISTI DEL 900 A PIANELLO VAL TIDONE",
|
||||
"title_en": "GLI ARTISTI DEL 900 A PIANELLO",
|
||||
"description_it": "L\u2019itinerario vuole far conoscere l\u2019arte e gli artisti che nel \u2018900 hanno operato in Val Tidone e si sono lasciati ispirare da questo territorio. Si tratta di artisti piacentini, talvolta provenienti proprio dalla Val Tidone, come Franco Corradini originario di Borgonovo, e addirittura dal paese di Pianello V.T., come \u00e8 il caso di Paolo Vincenzo Novara, che abit\u00f2 e lavor\u00f2 nel borgo. \r\n\r\nLa maggior parte del percorso si svolge a Pianello V.T., dove si visitano alcuni spazi della Rocca Dal Verme, il monumento ai Caduti, una cappella del Cimitero, la Chiesa parrocchiale, la Cappella di Lourdes e un mistadello ovvero una piccola cappella votiva. \r\n\r\nAl di fuori del paese, due tappe del percorso portano alla Rocca d\u2019Olgisio e all\u2019oratorio di Roccapulzana. ",
|
||||
"description_en": "The itinerary aims to raise awareness of the art and artists who worked in Val Tidone in the 1900s and were inspired by this area. These are artists from Piacenza, sometimes coming from Val Tidone, such as Franco Corradini originally from Borgonovo, and even from the town of Pianello V.T., as is the case of Paolo Vincenzo Novara, who lived and worked in the village.\r\n\r\nMost of the route takes place in Pianello V.T., where you visit some spaces of the Rocca Dal Verme, the war memorial, a chapel in the cemetery, the parish church, the Lourdes chapel and a mistadello or a small votive chapel.\r\n\r\nOutside the town, two stages of the route lead to the Rocca d'Olgisio and the oratory of Roccapulzana.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"cover": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title_it": "NELLE TERRE DI CUNIZA",
|
||||
"title_en": "IN CUNIZA'S LAND",
|
||||
"description_it": "Ricca di rinvenimenti archeologici, la Val Tidone fu frequentata sin dal Paleolitico e fu densamente abitata per tutta la sua storia, in particolare nell\u2019Alto Medioevo, per il quale abbiamo le testimonianze archeologiche ma anche parti di edifici ancora conservati. Notizie si ricavano dai nomi dei luoghi e dai documenti antichi. Tra questi, fondamentale \u00e8 il documento di vendita del 1033 in cui Cuniza, donna di legge longobarda, vende alcuni terreni dell\u2019area collinare di Pianello: questo ci permette di ricostruire la fisionomia di alcuni luoghi e di tracciare una possibile strada che congiungeva Pianello con Travo e Bobbio. Il percorso percorre in parte questa strada e passa nei luoghi di propriet\u00e0 della famiglia di Cuniza, non dimenticando il Museo Archeologico, che conserva reperti del periodo.",
|
||||
"description_en": "Rich in archaeological finds, Val Tidone was frequented since the Paleolithic and was densely inhabited throughout its history, particularly in the Early Middle Ages, for which we have archaeological evidence but also parts of buildings still preserved. Information is obtained from place names and ancient documents. Among these, fundamental is the sales document from 1033 in which Cuniza, a woman of Lombard law, sells some land in the hilly area of Pianello: this allows us to reconstruct the physiognomy of some places and to trace a possible road that connected Pianello with Travo and Bobbio. The route partly follows this road and passes through places owned by the Cuniza family, not forgetting the Archaeological Museum, which preserves finds from the period.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"cover": null
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title_it": "NEI LUOGHI DEI PARTIGIANI",
|
||||
"title_en": "IN THE PLACES OF THE PARTISANS",
|
||||
"description_it": "A partire dall\u2019autunno del \u201843 si costitu\u00ec a Piacenza il Comitato di Liberazione antifascista che organizz\u00f2 gruppi armati di resistenza. L\u2019Alta e Media Val Tidone rappresent\u00f2 uno dei territori cardine della lotta partigiana piacentina e fu teatro di alcuni episodi che hanno segnato, purtroppo anche in modo negativo, come nel caso dell\u2019eccidio di Str\u00e0, questa parte di storia piacentina. Il nostro territorio vide la presenza di alcuni gruppi armati di grande importanza, con alcuni dei personaggi pi\u00f9 famosi nella storia della Liberazione piacentina, come il \u2018Fausto\u2019, il \u2018Valoroso\u2019, il \u2018Ballonaio\u2019. Pianello e la Rocca d\u2019Olgisio, insieme all\u2019alta Valle del Tidoncello e l\u2019alta Val Luretta, ebbero un ruolo attivo nei fatti svoltisi qui tra l\u2019inizio del \u201844 e la Liberazione.",
|
||||
"description_en": "Starting from the autumn of 1943, the anti-fascist Liberation Committee was formed in Piacenza and organized armed resistance groups. The Upper and Middle Val Tidone represented one of the key territories of the Piacenza partisan struggle and was the scene of some episodes that marked, unfortunately also in a negative way, as in the case of the Str\u00e0 massacre, this part of Piacenza history. Our territory saw the presence of some armed groups of great importance, with some of the most famous characters in the history of the Piacenza Liberation, such as the 'Fausto', the 'Valoroso', the 'Ballonaio'. Pianello and the Rocca d'Olgisio, together with the upper Tidoncello Valley and the upper Luretta Valley, played an active role in the events that took place here between the beginning of '44 and the Liberation.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"cover": null
|
||||
}
|
||||
]
|
|
@ -1,41 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name_it": "Natura",
|
||||
"name_en": "Nature",
|
||||
"description_it": "Giri nella natura",
|
||||
"description_en": "Countryside routes",
|
||||
"icon": "",
|
||||
"color": "#de0e1b",
|
||||
"cover": "/montagne.png",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name_it": "Storia",
|
||||
"name_en": "History",
|
||||
"description_it": "Giri nella storia",
|
||||
"description_en": "History routes",
|
||||
"icon": "",
|
||||
"color": "#f6ae04",
|
||||
"cover": "/archi.png",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name_it": "Enogastronomia e tradizione",
|
||||
"name_en": "Tradition, food and wine",
|
||||
"description_it": "Giri nella tradizione locale",
|
||||
"description_en": "Culinarian and traditional routes",
|
||||
"icon": "",
|
||||
"color": "#213c8b",
|
||||
"cover": "/bibbito.png",
|
||||
"created_at": null,
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
}
|
||||
]
|
|
@ -1,161 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title_it": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"title_en": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"description_it": "Il percorso pone l\u2019attenzione su diversi borghi situati lungo il corso del Tidone. L\u2019insediamento umano in Val Tidone \u00e8 testimoniato fin dalle epoche pi\u00f9 remote, quando l\u2019uomo, in base alle necessit\u00e0 delle varie epoche, sceglie terrazzi fluviali o luoghi pi\u00f9 riparati per impiantare i propri siti abitati. \r\n\r\nL\u2019itinerario partendo da Arcello, passando per Pianello V.T., e risalendo il corso del torrente fino a Vallerenzo, incontra numerosi borghi, dove rinvenimenti archeologici testimoniano la presenza romana e lo sfruttamento agricolo dell\u2019area. In epoca successiva, la costruzione di chiese, castelli e monasteri fa comprendere l\u2019importanza strategica del territorio durante tutto il Medioevo, nelle lotte per il potere e come snodo degli itinerari di commercio e di pellegrinaggio.",
|
||||
"description_en": "The route focuses on various villages located along the Tidone. Human settlement in Val Tidone has been witnessed since the most remote times, when man, based on the needs of the various eras, chose river terraces or more sheltered places to establish his own inhabited sites.\r\n\r\nThe itinerary starting from Arcello, passing through Pianello V.T., and going up the course of the stream to Vallerenzo, encounters numerous villages, where archaeological finds testify to the Roman presence and the agricultural exploitation of the area. In the subsequent era, the construction of churches, castles and monasteries makes us understand the strategic importance of the territory throughout the Middle Ages, in the struggles for power and as a hub for trade and pilgrimage itineraries.",
|
||||
"length": 33600,
|
||||
"elevation_gain": 772,
|
||||
"sports": [
|
||||
{ "id": 1, "name_it": "Trekking", "name_en": "Trekking" },
|
||||
{ "id": 2, "name_it": "Cicloturismo", "name_en": "Cycle tourism" }
|
||||
],
|
||||
"category_id": 2,
|
||||
"category_name_it": "Storia",
|
||||
"category_name_en": "History"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title_it": "GLI ARTISTI DEL 900 A PIANELLO VAL TIDONE",
|
||||
"title_en": "GLI ARTISTI DEL 900 A PIANELLO",
|
||||
"description_it": "L\u2019itinerario vuole far conoscere l\u2019arte e gli artisti che nel \u2018900 hanno operato in Val Tidone e si sono lasciati ispirare da questo territorio. Si tratta di artisti piacentini, talvolta provenienti proprio dalla Val Tidone, come Franco Corradini originario di Borgonovo, e addirittura dal paese di Pianello V.T., come \u00e8 il caso di Paolo Vincenzo Novara, che abit\u00f2 e lavor\u00f2 nel borgo. \r\n\r\nLa maggior parte del percorso si svolge a Pianello V.T., dove si visitano alcuni spazi della Rocca Dal Verme, il monumento ai Caduti, una cappella del Cimitero, la Chiesa parrocchiale, la Cappella di Lourdes e un mistadello ovvero una piccola cappella votiva. \r\n\r\nAl di fuori del paese, due tappe del percorso portano alla Rocca d\u2019Olgisio e all\u2019oratorio di Roccapulzana. ",
|
||||
"description_en": "The itinerary aims to raise awareness of the art and artists who worked in Val Tidone in the 1900s and were inspired by this area. These are artists from Piacenza, sometimes coming from Val Tidone, such as Franco Corradini originally from Borgonovo, and even from the town of Pianello V.T., as is the case of Paolo Vincenzo Novara, who lived and worked in the village.\r\n\r\nMost of the route takes place in Pianello V.T., where you visit some spaces of the Rocca Dal Verme, the war memorial, a chapel in the cemetery, the parish church, the Lourdes chapel and a mistadello or a small votive chapel.\r\n\r\nOutside the town, two stages of the route lead to the Rocca d'Olgisio and the oratory of Roccapulzana.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 2,
|
||||
"category_name_it": "Storia",
|
||||
"category_name_en": "History"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title_it": "NELLE TERRE DI CUNIZA",
|
||||
"title_en": "IN CUNIZA'S LAND",
|
||||
"description_it": "Ricca di rinvenimenti archeologici, la Val Tidone fu frequentata sin dal Paleolitico e fu densamente abitata per tutta la sua storia, in particolare nell\u2019Alto Medioevo, per il quale abbiamo le testimonianze archeologiche ma anche parti di edifici ancora conservati. Notizie si ricavano dai nomi dei luoghi e dai documenti antichi. Tra questi, fondamentale \u00e8 il documento di vendita del 1033 in cui Cuniza, donna di legge longobarda, vende alcuni terreni dell\u2019area collinare di Pianello: questo ci permette di ricostruire la fisionomia di alcuni luoghi e di tracciare una possibile strada che congiungeva Pianello con Travo e Bobbio. Il percorso percorre in parte questa strada e passa nei luoghi di propriet\u00e0 della famiglia di Cuniza, non dimenticando il Museo Archeologico, che conserva reperti del periodo.",
|
||||
"description_en": "Rich in archaeological finds, Val Tidone was frequented since the Paleolithic and was densely inhabited throughout its history, particularly in the Early Middle Ages, for which we have archaeological evidence but also parts of buildings still preserved. Information is obtained from place names and ancient documents. Among these, fundamental is the sales document from 1033 in which Cuniza, a woman of Lombard law, sells some land in the hilly area of Pianello: this allows us to reconstruct the physiognomy of some places and to trace a possible road that connected Pianello with Travo and Bobbio. The route partly follows this road and passes through places owned by the Cuniza family, not forgetting the Archaeological Museum, which preserves finds from the period.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 2,
|
||||
"category_name_it": "Storia",
|
||||
"category_name_en": "History"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title_it": "NEI LUOGHI DEI PARTIGIANI",
|
||||
"title_en": "IN THE PLACES OF THE PARTISANS",
|
||||
"description_it": "A partire dall\u2019autunno del \u201843 si costitu\u00ec a Piacenza il Comitato di Liberazione antifascista che organizz\u00f2 gruppi armati di resistenza. L\u2019Alta e Media Val Tidone rappresent\u00f2 uno dei territori cardine della lotta partigiana piacentina e fu teatro di alcuni episodi che hanno segnato, purtroppo anche in modo negativo, come nel caso dell\u2019eccidio di Str\u00e0, questa parte di storia piacentina. Il nostro territorio vide la presenza di alcuni gruppi armati di grande importanza, con alcuni dei personaggi pi\u00f9 famosi nella storia della Liberazione piacentina, come il \u2018Fausto\u2019, il \u2018Valoroso\u2019, il \u2018Ballonaio\u2019. Pianello e la Rocca d\u2019Olgisio, insieme all\u2019alta Valle del Tidoncello e l\u2019alta Val Luretta, ebbero un ruolo attivo nei fatti svoltisi qui tra l\u2019inizio del \u201844 e la Liberazione.",
|
||||
"description_en": "Starting from the autumn of 1943, the anti-fascist Liberation Committee was formed in Piacenza and organized armed resistance groups. The Upper and Middle Val Tidone represented one of the key territories of the Piacenza partisan struggle and was the scene of some episodes that marked, unfortunately also in a negative way, as in the case of the Str\u00e0 massacre, this part of Piacenza history. Our territory saw the presence of some armed groups of great importance, with some of the most famous characters in the history of the Piacenza Liberation, such as the 'Fausto', the 'Valoroso', the 'Ballonaio'. Pianello and the Rocca d'Olgisio, together with the upper Tidoncello Valley and the upper Luretta Valley, played an active role in the events that took place here between the beginning of '44 and the Liberation.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 2,
|
||||
"category_name_it": "Storia",
|
||||
"category_name_en": "History"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title_it": "SULLE TRACCE DELLA GALEINA GRISA",
|
||||
"title_en": "ON THE FOOTSTEPS OF GALEINA GRISA",
|
||||
"description_it": "Nella notte della Galeina Grisa, tra il 30 aprile e il primo di maggio \u00e8 tradizione che giovani e vecchi si riuniscano per raggiungere le principali localit\u00e0 della valle per una lunga nottata di festeggiamenti, durante la quale le compagnie di cantori itineranti visitano osterie e cascine proponendo i loro canti goliardici inneggianti alla primavera. In cambio, essi ottengono spuntini a base di salame, uova e cipollotti (i cosiddetti \u2018bavaroni\u2019 in espressione dialettale) annaffiati da abbondante vino rosso, spesso servito in un\u2019unica coppa condivisa da tutti. \r\n\r\nIl percorso ripercorre i luoghi della tradizione che sono soliti accogliere i cantori e profumare di ospitalit\u00e0 e di convivialit\u00e0 agreste, ma consente anche di godere numerosi punti panoramici.",
|
||||
"description_en": "On the night of Galeina Grisa, between April 30th and May 1st, it is traditional for young and old to come together to reach the main towns of the valley for a long night of celebrations, during which companies of itinerant singers visit taverns and farmhouses proposing their playful songs praising spring. In exchange, they receive snacks based on salami, eggs and spring onions (the so-called 'bavaroni' in dialect) washed down with abundant red wine, often served in a single cup shared by all.\r\n\r\nThe route retraces the traditional places that usually welcome the singers and smell of hospitality and rural conviviality, but also allows you to enjoy numerous panoramic points.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 3,
|
||||
"category_name_it": "Enogastronomia e tradizione",
|
||||
"category_name_en": "Tradition, food and wine"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title_it": "PERCORSO BOTTEGHE STORICHE",
|
||||
"title_en": "HISTORICAL WORKSHOPS ROUTE",
|
||||
"description_it": "Un rilassante percorso urbano da effettuare a piedi o in bicicletta, tra le prelibate offerte enogastronomiche del paese; in ognuna delle tappe si possono trovare i prodotti tipici della tradizione piacentina e valtidonese ed essere coccolati dalle atmosfere antiche e dai profumi delle varie botteghe paesane. Un tour per golosi ma anche per chi vuole portare con s\u00e9 uno squisito ricordo della giornata in Val Tidone. \r\n\r\nLa bellissima piazza porticata di Pianello e le sue strette viuzze colorate accolgono inoltre numerosi bar che propongono aperitivi, cocktails, gelati e frapp\u00e8, che non potranno che rendere ancora pi\u00f9 piacevole la visita al paese.",
|
||||
"description_en": "A relaxing urban route to be taken on foot or by bicycle, among the delicious food and wine offerings of the town; in each of the stages you can find the typical products of the Piacenza and Valtidone tradition and be pampered by the ancient atmospheres and scents of the various village shops. A tour for gourmands but also for those who want to take with them an exquisite memory of the day in Val Tidone.\r\n\r\nThe beautiful porticoed square of Pianello and its narrow colorful streets also host numerous bars offering aperitifs, cocktails, ice creams and milkshakes, which will only make a visit to the town even more pleasant.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 3,
|
||||
"category_name_it": "Enogastronomia e tradizione",
|
||||
"category_name_en": "Tradition, food and wine"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title_it": "UN ANELLO DI CRINALI E COLORI",
|
||||
"title_en": "A RING OF RIDGES AND COLOURS",
|
||||
"description_it": "Dal cimitero di Arcello si parte in direzione sud, mantenendo alle spalle il paese. Dopo circa 300 m lungo la strada asfaltata, si arriva in prossimit\u00e0 della Tenuta Santa Giustina. Qui si imbocca il bivio a destra e da qui si continua a camminare lungo un percorso ad anello che si sviluppa lungo il crinale, salendo gradualmente fino al punto pi\u00f9 alto di questo percorso panoramico. Da qui comincia la discesa, attraverso il bosco di Santa Giustina, fino a raggiungere il lato posteriore della Tenuta. Il percorso si ricongiunge poi chiudendo l\u2019anello in prossimit\u00e0 del primo bivio incontrato. Da qui si riprende la strada asfaltata per tornare al cimitero di Arcello.",
|
||||
"description_en": "From the Arcello cemetery you set off in a southerly direction, keeping the town behind you. After about 300 m along the asphalt road, you arrive near the Tenuta Santa Giustina. Here you take the fork on the right and from here you continue walking along a circular route that develops along the ridge, gradually climbing up to the highest point of this panoramic route. From here the descent begins, through the Santa Giustina forest, until reaching the back side of the estate. The route then rejoins, closing the ring near the first crossroads encountered. From here we take the asphalt road back to the Arcello cemetery.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title_it": "LUNGO LE SPONDE DEL TIDONE",
|
||||
"title_en": "ALONG THE BANKS OF THE TIDONE",
|
||||
"description_it": "Partendo da Casa Nova si percorre la strada asfaltata per un breve tratto, in direzione Pradaglia. Dopo circa 400 m si svolta a destra, scendendo verso il Tidone costeggiando un vigneto. Arrivati in prossimit\u00e0 del torrente, si attraversa il letto del torrente in prossimit\u00e0 del guado, raggiungendo la sponda sinistra del torrente stesso (Comune Alta Val Tidone). Si prosegue in direzione sinistra lungo il Sentiero del Tidone (direzione Sorgente) per circa 2 Km. Superato il Mulino del Ceppetto si guada di nuovo il torrente, per tornate sulla sponda destra. Da qui si risale verso Casa Barbieri, si imbocca una stradina di ghiaia in direzione Pradaglia. Attraversata questa localit\u00e0, si prosegue in direzione Casa Nova per richiudere l\u2019anello, lungo quest\u2019ultimo tratto di strada asfaltata a bassa percorrenza.",
|
||||
"description_en": "Starting from Casa Nova, follow the asphalt road for a short stretch, towards Pradaglia. After about 400 m, turn right, descending towards the Tidone along a vineyard. Once you arrive near the stream, cross the bed of the stream near the ford, reaching the left bank of the stream itself (Alta Val Tidone Municipality). Continue in a left direction along the Sentiero del Tidone (direction Sorgente) for about 2 km. After passing the Mulino del Ceppetto you cross the stream again, to return to the right bank. From here you go up towards Casa Barbieri, take a gravel road towards Pradaglia. Once you have crossed this location, continue in the direction of Casa Nova to close the ring, along this last stretch of low-traffic asphalt road.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title_it": "DOVE LO SPIRITO AGRESTE INCONTRA LA GENUINIT\u00c0 DI UN CONTESTO RURALE",
|
||||
"title_en": "WHERE THE RURAL SPIRIT MEETS THE GENUINITY OF A RURAL CONTEXT",
|
||||
"description_it": "Partendo da Casa Nova si raggiunge Localit\u00e0 La Scabbia lungo un tragitto di 700 m. Da qui il percorso si sviluppa lungo un anello che sale lungo il versante est della vallata, raggiungendo la chiesetta di Madonna del Sasso e continua poi fino a raggiungere il crinale in Localit\u00e0 Gabbiano. Avanzando in costa, mantenendo la Rocca d\u2019Olgisio davanti a sinistra, si inizia la discesa lungo lo stesso versante, passando questa volta per localit\u00e0 Carbonara, e poi via via si scende tra vigneti, campi di grano e sentieri ombreggiati fino ad arrivare di nuovo in Localit\u00e0 La Scabbia, dove l\u2019anello si chiude. Da qui si ritorna a Casa Nova, percorrendo di nuovo il tragitto iniziale di 700 m.",
|
||||
"description_en": "Starting from Casa Nova you reach Localit\u00e0 La Scabbia along a 700 m journey. From here the route develops along a ring that climbs along the eastern side of the valley, reaching the small church of Madonna del Sasso and then continues until reaching the ridge in Localit\u00e0 Gabbiano. Advancing along the coast, keeping the Rocca d'Olgisio in front on the left, you begin the descent along the same side, this time passing through Carbonara, and then gradually descend through vineyards, wheat fields and shaded paths until you arrive again in Localit\u00e0 La Scabbia, where the ring closes. From here you return to Casa Nova, following the initial 700 m journey again.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title_it": "LE ARENARIE TRA I BOSCHI DELLA VAL CHIARONE",
|
||||
"title_en": "THE SANDSTONES AMONG THE WOODS OF THE CHIARONE VALLEY",
|
||||
"description_it": "Il percorso comincia in prossimit\u00e0 dell\u2019inizio del sentiero CAI 209, lungo la Val Chiarone. Imboccando tale sentiero si inizia a salire, fino a raggiungere un boschetto all\u2019interno del quale si sviluppa gran parte del percorso. Dopo circa 1 Km si interseca la bretella che raggiunge la piana di S. Martino. Questa bretella \u00e8 un ampio sentiero pianeggiante che si imbocca sulla sinistra. Attraversata la piana di S. Martino si riprende il sentiero CAI 211che porta al caratteristico Becco del Merlo, un corridoio tra due importanti rocce, lungo circa 20 m. Il sentiero prosegue ad anello ricongiungendosi al bivio dove si \u00e8 imboccata la bretella per la Piana di S. Martino. Da qui si imbocca la discesa, ripercorrendo il percorso gi\u00e0 fatto in precedenza lungo il sentiero CAI 209.",
|
||||
"description_en": "The route begins near the start of the CAI 209 path, along the Val Chiarone. Taking this path you begin to climb until you reach a grove within which much of the route develops. After about 1 km the road that reaches the S. Martino plain intersects. This link road is a wide flat path that you take on the left. After crossing the S. Martino plain, take the CAI 211 path again which leads to the characteristic Becco del Merlo, a corridor between two important rocks, approximately 20 m long. The path continues in a ring, rejoining the crossroads where you took the slip road to the Piana di S. Martino. From here take the descent, retracing the route already taken previously along the CAI 209 path.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title_it": "NATURA AUTENTICA SUL MONTE SERENO",
|
||||
"title_en": "AUTHENTIC NATURE ON MONTE SERENO",
|
||||
"description_it": "Il percorso si sviluppa lungo l\u2019anello contrassegnato dal sentiero CAI 225. Partendo da Ca\u2019 del Fabbro, si imbocca il sentiero, dopo circa 200 m si svola a sinistra e si inizia a salire lungo il versante, lungo un percorso sdrucciolevole caratterizzato da ghiaione. Arrivati sul crinale, dove ci sono diversi punti in cui fioriscono le orchidee tipiche dell\u2019Appennino nella prima met\u00e0 di maggio, si prosegue mantenendo il sentiero CAI225, fino a entrare nel bosco. Qui ci sono un paio di bivi a cui prestare attenzione. Ad un certo punto il sentiero si apre su una radura, il monte Sereno, da cui si pu\u00f2 vedere la Val Tebbia. Da qui si comincia a scendere, attraversando ancora tratti boschivi, alternati a brevi mulattiere. L\u2019ultima parte del percorso \u00e8 fitta di vegetazione, bisogna prestare attenzione ai segnali e presenta alcune criticit\u00e0 nella discesa: il sentiero si restringe e la ripidit\u00e0 aumenta. Arrivati nel fondovalle, si imbocca la strada asfaltata per richiudere l\u2019anello in localit\u00e0 C\u00e0 del Fabbro.",
|
||||
"description_en": "The route develops along the ring marked by the CAI path 225. Starting from Ca' del Fabbro, take the path, after about 200 m you turn left and start climbing along the slope, along a slippery path characterized by scree . Once you reach the ridge, where there are several points where the typical orchids of the Apennines bloom in the first half of May, continue keeping the CAI225 path until you enter the woods. There are a couple of crossroads to watch out for here. At a certain point the path opens onto a clearing, Mount Sereno, from which you can see Val Tebbia. From here you begin to descend, still crossing wooded sections, alternating with short mule tracks. The last part of the route is thick with vegetation, you must pay attention to the signs and presents some critical issues in the descent: the path narrows and the steepness increases. Once you reach the valley floor, take the asphalt road to close the ring in the C\u00e0 del Fabbro area.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title_it": "L\u2019AGRICOLTURA IN VAL TIDONE, VIGNETI, ULIVI E CAMPI COLTIVATI",
|
||||
"title_en": "AGRICULTURE IN VAL TIDONE, VINEYARDS, OLIVE TREES AND CULTIVATED FIELDS",
|
||||
"description_it": "Dal cimitero di Arcello si parte in direzione sud, mantenendo alle spalle il paese. Dopo circa 300 m lungo la strada asfaltata, si arriva in prossimit\u00e0 della Tenuta Santa Giustina. Qui si imbocca il bivio a destra e da qui si continua a camminare lungo il crinale. Dopo circa 2 Km si incontra un bivio: proseguendo a sinistra continua il percorso N2, mantenendo la destra si continua sul percorso T1. La carraia inizia a scendere dolcemente, fino alla localit\u00e0 rurale di Poggio Cavalli. Si imbocca qui la piccola strada asfaltata che, sempre in discesa, porta fino al Podere Ca\u2019 Nova, dove termina il percorso. Quest\u2019ultimo tratto \u00e8 circondato da vigneti e incontra uno degli uliveti della Val Tidone.",
|
||||
"description_en": "From the Arcello cemetery you set off in a southerly direction, keeping the town behind you. After about 300 m along the asphalt road, you arrive near the Tenuta Santa Giustina. Here you take the fork on the right and from here you continue walking along the ridge. After about 2 km you come to a crossroads: continuing on the left, continue on route N2, keeping to the right, continue on route T1. The carriage road begins to descend gently, up to the rural town of Poggio Cavalli. Here you take the small asphalt road which, always downhill, leads to Podere Ca' Nova, where the route ends. This last stretch is surrounded by vineyards and meets one of the olive groves of Val Tidone.",
|
||||
"length": 0,
|
||||
"elevation_gain": 0,
|
||||
"sports": [],
|
||||
"category_id": 1,
|
||||
"category_name_it": "Natura",
|
||||
"category_name_en": "Nature"
|
||||
}
|
||||
]
|
|
@ -1,35 +0,0 @@
|
|||
{
|
||||
"id": 1,
|
||||
"name_it": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"name_en": "GLI INSEDIAMENTI ANTICHI LUNGO IL TIDONE",
|
||||
"description_it": "Il percorso pone l\u2019attenzione su diversi borghi situati lungo il corso del Tidone. L\u2019insediamento umano in Val Tidone \u00e8 testimoniato fin dalle epoche pi\u00f9 remote, quando l\u2019uomo, in base alle necessit\u00e0 delle varie epoche, sceglie terrazzi fluviali o luoghi pi\u00f9 riparati per impiantare i propri siti abitati. \r\n\r\nL\u2019itinerario partendo da Arcello, passando per Pianello V.T., e risalendo il corso del torrente fino a Vallerenzo, incontra numerosi borghi, dove rinvenimenti archeologici testimoniano la presenza romana e lo sfruttamento agricolo dell\u2019area. In epoca successiva, la costruzione di chiese, castelli e monasteri fa comprendere l\u2019importanza strategica del territorio durante tutto il Medioevo, nelle lotte per il potere e come snodo degli itinerari di commercio e di pellegrinaggio.",
|
||||
"description_en": "The route focuses on various villages located along the Tidone. Human settlement in Val Tidone has been witnessed since the most remote times, when man, based on the needs of the various eras, chose river terraces or more sheltered places to establish his own inhabited sites.\r\n\r\nThe itinerary starting from Arcello, passing through Pianello V.T., and going up the course of the stream to Vallerenzo, encounters numerous villages, where archaeological finds testify to the Roman presence and the agricultural exploitation of the area. In the subsequent era, the construction of churches, castles and monasteries makes us understand the strategic importance of the territory throughout the Middle Ages, in the struggles for power and as a hub for trade and pilgrimage itineraries.",
|
||||
"route_category_id": 2,
|
||||
"created_at": "2023-11-02T10:50:07.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null,
|
||||
"route_sport_details": [
|
||||
{
|
||||
"id": 1,
|
||||
"route_id": 1,
|
||||
"sport_id": 1,
|
||||
"short_description_it": "Percorso escursionistico intermedio. Buon allenamento richiesto. Sentieri facilmente percorribili. Adatto a ogni livello di abilit\u00e0. ",
|
||||
"short_description_en": "Intermediate hiking route. Good training required. Easily accessible paths. Suitable for all skill levels.",
|
||||
"gpx_path": "",
|
||||
"distance": 16800,
|
||||
"duration": 288,
|
||||
"elevation_gain": 439,
|
||||
"elevation_loss": null,
|
||||
"altitude_max": 620,
|
||||
"altitude_min": 180,
|
||||
"difficulty_it": "Facile",
|
||||
"difficulty_en": "Easy",
|
||||
"route_type_it": "Percorso ad anello",
|
||||
"route_type_en": "Ring route",
|
||||
"created_at": "2023-11-02T10:57:41.000000Z",
|
||||
"updated_at": null,
|
||||
"deleted_at": null
|
||||
}
|
||||
],
|
||||
"pictures": []
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
const API_URL = `${PUBLIC_BACKEND_URL}/api`;
|
||||
|
||||
const getAllRoutes = async () => {
|
||||
let data = [];
|
||||
|
||||
const response = await fetch(`${API_URL}/all-routes`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getRouteCategories = async () => {
|
||||
let data = [];
|
||||
|
||||
const response = await fetch(`${API_URL}/route-categories`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getRouteByCategory = async (categoryId: number) => {
|
||||
let data = [];
|
||||
const response = await fetch(`${API_URL}/route-by-category/${categoryId}`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getRoute = async (routeId: number) => {
|
||||
let data = {};
|
||||
const response = await fetch(`${API_URL}/route/${routeId}`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getSport = async (routeId: number, sportId: number) => {
|
||||
let data = {};
|
||||
const response = await fetch(`${API_URL}/route/${routeId}/${sportId}`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getPlacemarks = async (routeId: number, sportId: number) => {
|
||||
let data = {};
|
||||
const response = await fetch(`${API_URL}/getPlacemarks/${routeId}/${sportId}`);
|
||||
const json = await response.json();
|
||||
data = json;
|
||||
|
||||
return data;
|
||||
};
|
||||
export { getAllRoutes, getPlacemarks, getSport, getRoute, getRouteByCategory, getRouteCategories };
|
|
@ -1,45 +0,0 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
|
||||
<div id="container">
|
||||
<div class="wrapper">
|
||||
<p id="status">{$page.status}</p>
|
||||
<p id="message">{$page.error?.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
background-image: url(/splash.webp);
|
||||
background-size: cover;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
height: 100dvh;
|
||||
width: 100dvw;
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.wrapper {
|
||||
background: transparent;
|
||||
display: flex;
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 10px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#status {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
p {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||
export const prerender = false;
|
||||
export const ssr = true;
|
||||
export const csr = true;
|
|
@ -1,66 +0,0 @@
|
|||
<script lang="ts">
|
||||
import BottomAppBar from '$lib/components/BottomAppBar.svelte';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
let node: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
if (dev) return;
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
|
||||
if (BroadcastChannel) {
|
||||
const channel = new BroadcastChannel('sw-messages');
|
||||
channel.addEventListener('message', (event) => {
|
||||
new Toast({ target: node });
|
||||
});
|
||||
} else {
|
||||
navigator.serviceWorker.addEventListener('message', (evt) => {
|
||||
new Toast({ target: node });
|
||||
});
|
||||
}
|
||||
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration?.active?.postMessage('Save client');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onNavigate((navigation: { complete: any }) => {
|
||||
if (!document.startViewTransition) return;
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
document?.startViewTransition(async () => {
|
||||
resolve();
|
||||
await navigation.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={node}></div>
|
||||
|
||||
<main><slot /></main>
|
||||
<BottomAppBar />
|
||||
|
||||
<style>
|
||||
main {
|
||||
height: calc(100dvh - 10dvh - 10px);
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 70px auto;
|
||||
}
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Path from '$lib/components/Path.svelte';
|
||||
|
||||
export let data;
|
||||
let categories: App.Category[] = data.categories;
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div id="welcome-message">Benvenuti a <span class="bold">Pianello Val Tidone</span></div>
|
||||
<div id="route-cards">
|
||||
{#each categories as category}
|
||||
<Path src={category.cover} color={category.color} name={category.name_it} id={category.id} />
|
||||
{/each}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: grid;
|
||||
margin: 0 auto;
|
||||
height: calc(90dvh - 10px);
|
||||
width: 100%;
|
||||
font-family: 'Roboto-Regular';
|
||||
grid-template-rows: 70px auto;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
}
|
||||
#welcome-message {
|
||||
padding-top: 20px;
|
||||
font-size: 18px;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#route-cards {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +0,0 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
const API_URL = `${PUBLIC_BACKEND_URL}/api`;
|
||||
|
||||
export async function load({ fetch }) {
|
||||
let categories = [];
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/route-categories`);
|
||||
categories = await response.json();
|
||||
} catch (ex) {
|
||||
error(404, { message: 'API Not Found' });
|
||||
}
|
||||
|
||||
return { categories };
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Route from '$lib/components/Route.svelte';
|
||||
import HomeHeader from '$lib/components/HomeHeader.svelte';
|
||||
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<HomeHeader title={data.category}></HomeHeader>
|
||||
|
||||
<div>
|
||||
{#each data.routes as route}
|
||||
<Route {route}></Route>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
padding-top: 10px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
</style>
|
|
@ -1,26 +0,0 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
const API_URL = `${PUBLIC_BACKEND_URL}/api`;
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
const response = await fetch(`${API_URL}/route-categories`);
|
||||
const categories: App.Category[] = await response.json();
|
||||
const categoryId = Number(params.slug);
|
||||
|
||||
const category: App.Category = categories.find((c) => c.id === categoryId) as App.Category;
|
||||
|
||||
if (!category) {
|
||||
error(404, { message: 'Path not found' });
|
||||
}
|
||||
|
||||
const response2 = await fetch(`${API_URL}/route-by-category/${categoryId}`);
|
||||
const routes = await response2.json();
|
||||
|
||||
const toReturn = {
|
||||
category: category.name_it,
|
||||
routes
|
||||
};
|
||||
|
||||
return toReturn;
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import Tabs from '$lib/components/Tabs.svelte';
|
||||
import MapTab from '$lib/components/tabs/MapTab.svelte';
|
||||
|
||||
export let data: App.Route;
|
||||
|
||||
let divider: HTMLDivElement;
|
||||
let isMap: boolean = false;
|
||||
|
||||
const tabClick = (event) => {
|
||||
isMap = event.detail.component === MapTab;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Header></Header>
|
||||
<img src="/images/splash-background.webp" alt="splash" />
|
||||
|
||||
<div bind:this={divider} id="divider" class:move-to-top={isMap}>
|
||||
<div id="banner">
|
||||
<p class="path-name">Percorso <b>{data.name_it}</b></p>
|
||||
<p id="duration">Dislivello {data?.route_sport_details[0]?.elevation_gain} m</p>
|
||||
</div>
|
||||
<Tabs on:tab-click={tabClick} route={data}></Tabs>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#divider {
|
||||
background: white;
|
||||
margin-top: 0px;
|
||||
margin-top: 0;
|
||||
align-self: end;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.move-to-top {
|
||||
align-self: start !important;
|
||||
height: calc(90dvh - 80px);
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: calc(100% - 10vh - 10px);
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#banner {
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
background-color: #de0e1b;
|
||||
color: white;
|
||||
font-size: 1.3em;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
#banner p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#banner #duration {
|
||||
font-size: 14px;
|
||||
text-align: rightìì;
|
||||
}
|
||||
|
||||
.path-name {
|
||||
view-transition-name: title;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +0,0 @@
|
|||
import { error } from '@sveltejs/kit';
|
||||
import { PUBLIC_BACKEND_URL } from '$env/static/public';
|
||||
|
||||
const API_URL = `${PUBLIC_BACKEND_URL}/api`;
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
const routeId = Number(params.slug);
|
||||
const response = await fetch(`${API_URL}/route/${routeId}`);
|
||||
const route = await response.json();
|
||||
|
||||
if (!route) {
|
||||
error(404, { message: 'Route non found' });
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Settings
|
|
@ -1,80 +0,0 @@
|
|||
/// <reference types="@sveltejs/kit" />
|
||||
/// <reference no-default-lib="true"/>
|
||||
/// <reference lib="esnext" />
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import { build, files, version, prerendered } from '$service-worker';
|
||||
|
||||
// Create a unique cache name for this deployment
|
||||
const CACHE = `sw-cache-${version}`;
|
||||
|
||||
const ASSETS = [
|
||||
...build, // the app itself
|
||||
...files, // everything in `static`,
|
||||
...prerendered // dynamic routes
|
||||
];
|
||||
|
||||
let client;
|
||||
|
||||
addEventListener('message', (event) => {
|
||||
client = event.source;
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// Create a new cache and add all files to it
|
||||
async function addFilesToCache() {
|
||||
const cache = await caches.open(CACHE);
|
||||
await cache.addAll(ASSETS);
|
||||
}
|
||||
|
||||
event.waitUntil(addFilesToCache());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// Remove previous cached data from disk
|
||||
async function deleteOldCaches() {
|
||||
for (const key of await caches.keys()) {
|
||||
if (key !== CACHE) await caches.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
event.waitUntil(deleteOldCaches());
|
||||
let channel;
|
||||
if (BroadcastChannel) {
|
||||
channel = new BroadcastChannel('sw-messages');
|
||||
channel.postMessage({ title: 'Cache Downloaded' });
|
||||
} else {
|
||||
client.postMessage('Cache Downloaded');
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
// ignore POST requests etc
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
async function respond() {
|
||||
const url = new URL(event.request.url);
|
||||
const cache = await caches.open(CACHE);
|
||||
|
||||
// `build`/`files` can always be served from the cache
|
||||
if (ASSETS.includes(url.pathname)) {
|
||||
return cache.match(url.pathname);
|
||||
}
|
||||
|
||||
// for everything else, try the network first, but
|
||||
// fall back to the cache if we're offline
|
||||
try {
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (response.status === 200) {
|
||||
cache.put(event.request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
return cache.match(event.request);
|
||||
}
|
||||
}
|
||||
|
||||
event.respondWith(respond());
|
||||
});
|
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 8.1 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M620-740q-33 0-56.5-23.5T540-820q0-33 23.5-56.5T620-900q33 0 56.5 23.5T700-820q0 33-23.5 56.5T620-740ZM432-540l88 92v248h-80v-200L312-512q-14-12-19-25t-5-31q0-18 5.5-30.5T312-624l112-112q13-13 27.5-18.5T484-760q18 0 32.5 5.5T544-736l76 76q27 27 63 43.5t81 16.5v80q-63 0-114-22.5T560-604l-32-32-96 96Zm-232 60q85 0 142.5 57.5T400-280q0 85-57.5 142.5T200-80q-85 0-142.5-57.5T0-280q0-85 57.5-142.5T200-480Zm0 340q57 0 98.5-41.5T340-280q0-57-41.5-98.5T200-420q-57 0-98.5 41.5T60-280q0 57 41.5 98.5T200-140Zm560-340q85 0 142.5 57.5T960-280q0 85-57.5 142.5T760-80q-85 0-142.5-57.5T560-280q0-85 57.5-142.5T760-480Zm0 340q57 0 98.5-41.5T900-280q0-57-41.5-98.5T760-420q-57 0-98.5 41.5T620-280q0 57 41.5 98.5T760-140Z"/></svg>
|
Before Width: | Height: | Size: 813 B |
Before Width: | Height: | Size: 13 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 42.875 8.625 C 42.84375 8.632813 42.8125 8.644531 42.78125 8.65625 C 42.519531 8.722656 42.292969 8.890625 42.15625 9.125 L 21.71875 40.8125 L 7.65625 28.125 C 7.410156 27.8125 7 27.675781 6.613281 27.777344 C 6.226563 27.878906 5.941406 28.203125 5.882813 28.597656 C 5.824219 28.992188 6.003906 29.382813 6.34375 29.59375 L 21.25 43.09375 C 21.46875 43.285156 21.761719 43.371094 22.050781 43.328125 C 22.339844 43.285156 22.59375 43.121094 22.75 42.875 L 43.84375 10.1875 C 44.074219 9.859375 44.085938 9.425781 43.875 9.085938 C 43.664063 8.746094 43.269531 8.566406 42.875 8.625 Z"/></svg>
|
Before Width: | Height: | Size: 694 B |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 926 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 136 KiB |
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"id": "/",
|
||||
"name": "PianelloExperience",
|
||||
"short_name": "PianelloExperience",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#fff",
|
||||
"description": "PianelloExperience",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/app-icon-48x48.jpeg",
|
||||
"sizes": "48x48",
|
||||
"type": "image/jpeg"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-72x72.jpeg",
|
||||
"sizes": "72x72",
|
||||
"type": "image/jpeg"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-96x96.jpeg",
|
||||
"sizes": "96x96",
|
||||
"type": "image/jpeg"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-144x144.jpeg",
|
||||
"sizes": "144x144",
|
||||
"type": "image/jpeg"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-168x168.jpeg",
|
||||
"sizes": "168x168",
|
||||
"type": "image/jpeg"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/app-icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 292 KiB |
Before Width: | Height: | Size: 136 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m280-40 123-622q6-29 27-43.5t44-14.5q23 0 42.5 10t31.5 30l40 64q18 29 46.5 52.5T700-529v-71h60v560h-60v-406q-48-11-89-35t-71-59l-24 120 84 80v300h-80v-240l-84-80-72 320h-84Zm17-395-85-16q-16-3-25-16.5t-6-30.5l30-157q6-32 34-50.5t60-12.5l46 9-54 274Zm243-305q-33 0-56.5-23.5T460-820q0-33 23.5-56.5T540-900q33 0 56.5 23.5T620-820q0 33-23.5 56.5T540-740Z"/></svg>
|
Before Width: | Height: | Size: 457 B |
|
@ -1,22 +0,0 @@
|
|||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
files: {
|
||||
serviceWorker: 'src/sw.ts'
|
||||
},
|
||||
serviceWorker: {
|
||||
register: false
|
||||
},
|
||||
adapter: adapter({
|
||||
fallback: null,
|
||||
precompress: true,
|
||||
strict: true
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
|
||||
const config = {
|
||||
plugins: [sveltekit()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
},
|
||||
build: {
|
||||
minify: 'esbuild',
|
||||
target: 'esnext'
|
||||
},
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
precompress: false
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
29
index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" href="/static/images/home-icon.png">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<title>Pianello</title>
|
||||
|
||||
<link rel="stylesheet" href="/src/css/roboto.css">
|
||||
<link rel="stylesheet" href="/src/css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header></header>
|
||||
|
||||
<main></main>
|
||||
|
||||
<footer></footer>
|
||||
|
||||
<!-- <wd-loader></wd-loader> -->
|
||||
|
||||
<script src="/src/components/bottom-app-bar.js" type="module"></script>
|
||||
<script src="/src/js/index.js" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
21
manifest.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"id": "pianello-web-app",
|
||||
"short_name": "Pianello",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/images/app-icon-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/static/images/app-icon-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#de0e1b",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#de0e1b"
|
||||
}
|
15
package.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "pianello-web-app",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build && ./build-sw.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^4.3.2",
|
||||
"workbox-build": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0"
|
||||
}
|
||||
}
|
100
src/components/app-bar.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import app from '../js/app';
|
||||
|
||||
const appBarLogoURL = new URL('../../static/images/app-bar-logo.png', import.meta.url).href;
|
||||
const backArrowIconURL = new URL('../../static/images/back-arrow-icon.png', import.meta.url).href;
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<img id="back-button" src="${backArrowIconURL}">
|
||||
</div>
|
||||
<div id="title">
|
||||
Percorsi <span class="bold">naturalistici</span>
|
||||
</div>
|
||||
<div>
|
||||
<img src="${appBarLogoURL}">
|
||||
</div>
|
||||
`;
|
||||
const style = `
|
||||
:host {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 68px;
|
||||
width: 100%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:host > div {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#title {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#title span {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
// flex: 0 1 32px;
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Bottom app bar custom element.
|
||||
*/
|
||||
class AppBar extends HTMLElement {
|
||||
#elements = {};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#createShadowDOM();
|
||||
this.#setElements();
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shadow DOM.
|
||||
*/
|
||||
#createShadowDOM() {
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>${style}</style>
|
||||
${template}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all shadow dom selectors.
|
||||
*/
|
||||
#setElements() {
|
||||
this.#elements = {
|
||||
backButton: this.shadowRoot.querySelector('#back-button'),
|
||||
};
|
||||
}
|
||||
|
||||
#addEventListeners() {
|
||||
this.#elements.backButton.addEventListener('click', () => {
|
||||
app.router.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('app-bar', AppBar);
|
||||
|
||||
export default AppBar;
|
||||
|
80
src/components/bottom-app-bar.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
const homeIconURL = new URL('../../static/images/home-icon.png', import.meta.url).href;
|
||||
const routesIconURL = new URL('../../static/images/routes-icon.png', import.meta.url).href;
|
||||
const settingsIconURL = new URL('../../static/images/settings-icon.png', import.meta.url).href;
|
||||
|
||||
const template = `
|
||||
<div>
|
||||
<img src="${homeIconURL}">
|
||||
</div>
|
||||
<div>
|
||||
<img src="${routesIconURL}">
|
||||
</div>
|
||||
<div>
|
||||
<img src="${settingsIconURL}">
|
||||
</div>
|
||||
`;
|
||||
const style = `
|
||||
:host {
|
||||
display: flex;
|
||||
height: 68px;
|
||||
width: 90%;
|
||||
border-radius: 90px;
|
||||
box-shadow: 0 0 50px #ccc;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
:host > div {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Bottom app bar custom element.
|
||||
*/
|
||||
class BottomAppBar extends HTMLElement {
|
||||
#elements = {};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#createShadowDOM();
|
||||
this.#setElements();
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shadow DOM.
|
||||
*/
|
||||
#createShadowDOM() {
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>${style}</style>
|
||||
${template}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all shadow dom selectors.
|
||||
*/
|
||||
#setElements() {
|
||||
this.elements = {};
|
||||
}
|
||||
|
||||
#addEventListeners() {}
|
||||
}
|
||||
|
||||
customElements.define('bottom-app-bar', BottomAppBar);
|
||||
|
||||
export default BottomAppBar;
|
||||
|
223
src/components/wd-loader.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
const template = `
|
||||
<div class="background">
|
||||
<div class="loader">
|
||||
<svg viewBox="0 0 32 32" width="32" height="32">
|
||||
<circle id="spinner" cx="16" cy="16" r="14" fill="none"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
`;
|
||||
const style = `
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 99999;
|
||||
transition: opacity 0.333s cubic-bezier(0, 0, 0.21, 1);
|
||||
}
|
||||
|
||||
.loader {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
position: fixed;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
box-sizing: border-box;
|
||||
stroke: var(--accent-color);
|
||||
stroke-width: 3px;
|
||||
transform-origin: 50%;
|
||||
animation: line 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite, rotate 1.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes line {
|
||||
0% {
|
||||
stroke-dasharray: 2, 85.964;
|
||||
-webkit-transform: rotate(0);
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 65.973, 21.9911;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 2, 85.964;
|
||||
stroke-dashoffset: -65.973;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
#message {
|
||||
position: absolute;
|
||||
height: 20%;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background-color: #212121;
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
color: #eee;
|
||||
place-items: center;
|
||||
display: none;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
#message.show {
|
||||
display: grid !important;
|
||||
}
|
||||
|
||||
:host(.contained) .background {
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:host(.contained) .loader {
|
||||
position: absolute;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Spinner-style loader ce.
|
||||
*/
|
||||
class WDLoader extends HTMLElement {
|
||||
#elements = {};
|
||||
isVisible = false;
|
||||
#counter = 0;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#createShadowDOM();
|
||||
this.#setElements();
|
||||
getComputedStyle(this.#elements.background).opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set message.
|
||||
* @param {String} value
|
||||
*/
|
||||
set message(value) {
|
||||
if (value && value !== '') {
|
||||
this.#elements.message.innerHTML = value;
|
||||
this.#elements.message.classList.add('show');
|
||||
} else {
|
||||
this.#elements.message.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {WDLoader}
|
||||
*/
|
||||
static get instance() {
|
||||
let loader = document.querySelector('wd-loader');
|
||||
|
||||
if (loader) {
|
||||
return loader;
|
||||
}
|
||||
|
||||
loader = new WDLoader();
|
||||
document.body.appendChild(loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shadow DOM.
|
||||
*/
|
||||
#createShadowDOM() {
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>${style}</style>
|
||||
${template}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all shadow dom selectors.
|
||||
*/
|
||||
#setElements() {
|
||||
this.#elements = {
|
||||
background: this.shadowRoot.querySelector('.background'),
|
||||
message: this.shadowRoot.querySelector('#message'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loader.
|
||||
* @param {String} message
|
||||
*/
|
||||
show(message = '') {
|
||||
this.#counter++;
|
||||
this.style.display = 'block';
|
||||
this.isVisible = true;
|
||||
this.#elements.background.style.opacity = 1;
|
||||
this.message = message;
|
||||
getComputedStyle(this.#elements.background).opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loader.
|
||||
*/
|
||||
hide() {
|
||||
this.#counter--;
|
||||
|
||||
if (this.#counter > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onLoaderTransitionEnd = (evt) => {
|
||||
this.style.display = 'none';
|
||||
};
|
||||
|
||||
this.#elements.background.addEventListener(
|
||||
'transitionend',
|
||||
onLoaderTransitionEnd.bind(this),
|
||||
{once: true});
|
||||
|
||||
this.isVisible = false;
|
||||
this.message = '';
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.#elements.background.style.opacity = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('wd-loader', WDLoader);
|
||||
|
||||
export default WDLoader;
|
43
src/css/app.css
Normal file
|
@ -0,0 +1,43 @@
|
|||
|
||||
:root {
|
||||
--footer-height: calc(68px + 20px + 20px);
|
||||
--accent-color: #213c8b;
|
||||
--primary-color: #de0e1b;
|
||||
--pianello-red: #de0e1b;
|
||||
--pianello-yellow: #f6ae04;
|
||||
--pianello-blue: #213c8b;
|
||||
--card-background-color: #f9f4f1;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0px;
|
||||
background-color: #fff;
|
||||
font-family: 'Roboto-Regular';
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-rows: var(--footer-height) auto var(--footer-height);
|
||||
}
|
||||
|
||||
header,
|
||||
main,
|
||||
footer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
76
src/css/home-page.css
Normal file
|
@ -0,0 +1,76 @@
|
|||
/* @import './app.css'; */
|
||||
|
||||
#welcome-message {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#route-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.route-card {
|
||||
flex: 1;
|
||||
--route-card-radius: 45px;
|
||||
border-radius: var(--route-card-radius);
|
||||
box-shadow: 0 0 27px #ccc;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.route-card > * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.route-card-left {
|
||||
flex: 0 1 20%;
|
||||
border-top-left-radius: var(--route-card-radius);
|
||||
border-bottom-left-radius: var(--route-card-radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.route-card-left img {
|
||||
display: block;
|
||||
width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.route-card-right {
|
||||
flex: 0 1 20%;
|
||||
border-top-right-radius: var(--route-card-radius);
|
||||
border-bottom-right-radius: var(--route-card-radius);
|
||||
background-image: url('../../static/images/test-1.jpg');
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
#route-cards .route-card:nth-child(1) .route-card-left {
|
||||
background-color: var(--pianello-red);
|
||||
}
|
||||
|
||||
#route-cards .route-card:nth-child(2) .route-card-left {
|
||||
background-color: var(--pianello-yellow);
|
||||
}
|
||||
|
||||
#route-cards .route-card:nth-child(3) .route-card-left {
|
||||
background-color: var(--pianello-blue);
|
||||
}
|
||||
|
||||
.route-card .name {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.route-card .name > * {
|
||||
flex: 1;
|
||||
}
|
1
src/css/index.css
Normal file
|
@ -0,0 +1 @@
|
|||
@import './app.css';
|
5
src/css/roboto.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Roboto-Regular';
|
||||
src: url('../../static/fonts/roboto/Roboto-Regular.ttf') format('TrueType');
|
||||
}
|
2
src/css/routes-page.css
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
36
src/js/app.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import Router from './router.js';
|
||||
import WDLoader from '../components/wd-loader.js';
|
||||
import robotoFontStyle from '../css/roboto.css?raw';
|
||||
import appStyle from '../css/app.css?raw';
|
||||
const appIconURL = new URL('../../static/images/home-icon.png', import.meta.url).href;
|
||||
|
||||
const app = {
|
||||
title: 'Pianello',
|
||||
meta: {
|
||||
charset: 'utf-8',
|
||||
viewport: 'width=device-width',
|
||||
},
|
||||
link: {
|
||||
icon: appIconURL,
|
||||
},
|
||||
styleSheets: [robotoFontStyle, appStyle]
|
||||
.map((rawSheet) => {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(rawSheet);
|
||||
return sheet;
|
||||
}),
|
||||
page: null,
|
||||
router: new Router(),
|
||||
loader: WDLoader.instance,
|
||||
init: () => {
|
||||
document.head.innerHTML = `
|
||||
<meta charset="${app.meta.charset}">
|
||||
<link rel="icon" href="${app.link.icon}">
|
||||
<meta name="viewport" content="${app.meta.viewport}">
|
||||
`;
|
||||
document.title = app.title;
|
||||
document.adoptedStyleSheets = app.styleSheets;
|
||||
},
|
||||
};
|
||||
|
||||
export default app;
|
53
src/js/home-page.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import app from './app.js';
|
||||
import pageStyle from '../css/home-page.css?raw';
|
||||
import BottomAppBar from '../components/bottom-app-bar.js';
|
||||
import routesPageURL from '../pages/routes-page.html?url';
|
||||
|
||||
class HomePage {
|
||||
/**
|
||||
* An object containing useful HTMLElement references.
|
||||
* @type {Object}
|
||||
*/
|
||||
#elements = {};
|
||||
|
||||
constructor() {
|
||||
this.#addPageStyle();
|
||||
this.#setElements();
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get elements() {
|
||||
return this.#elements;
|
||||
}
|
||||
|
||||
#addPageStyle() {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(pageStyle);
|
||||
document.adoptedStyleSheets = [...app.styleSheets, sheet];
|
||||
}
|
||||
|
||||
/**
|
||||
* Init `#elements` field that holds element references.
|
||||
*/
|
||||
#setElements() {
|
||||
this.#elements = {
|
||||
routeCards: document.querySelector('#route-cards'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
#addEventListeners() {
|
||||
this.#elements.routeCards.addEventListener('click', () => {
|
||||
app.router.navigate(routesPageURL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default HomePage;
|
||||
|
||||
app.init();
|
||||
app.page = new HomePage();
|
13
src/js/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import app from './app.js';
|
||||
import homePageURL from '../pages/home-page.html?url';
|
||||
|
||||
app.init();
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Service worker build output path.
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
|
||||
app.router.navigate(homePageURL);
|
||||
});
|
162
src/js/router.js
Normal file
|
@ -0,0 +1,162 @@
|
|||
import app from './app.js';
|
||||
|
||||
/**
|
||||
* Router
|
||||
*/
|
||||
class Router {
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
this.host = location.host;
|
||||
this.previousURL = null;
|
||||
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all event listeners handles.
|
||||
*/
|
||||
initListeners() {
|
||||
document.addEventListener('click', this.onLinkClick.bind(this));
|
||||
window.addEventListener('popstate', this.onPopState.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* window popstate event handler.
|
||||
* @param {Event} event
|
||||
*/
|
||||
onPopState(event) {
|
||||
this.navigate(location.href, event.state.scrollTop, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTMLAnchorElement click event handler.
|
||||
* @param {Event} event
|
||||
*/
|
||||
async onLinkClick(event) {
|
||||
const anchor = event.target.closest('a');
|
||||
if (!anchor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (anchor.dataset.hasOwnProperty('external')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {href} = anchor;
|
||||
const link = new URL(href);
|
||||
|
||||
// If it’s an external link, just navigate.
|
||||
if (link.host !== this.host) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.navigate(link.toString())
|
||||
.catch((error) => console.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the navigation.
|
||||
* @param {string} link
|
||||
* @param {number} scrollTop
|
||||
* @param {boolean} pushState
|
||||
* @return {Promise<number>} Fetch fragment status
|
||||
*/
|
||||
async navigate(link, scrollTop = 0, pushState = true) {
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current route for history pop event.
|
||||
this.previousURL = location.href;
|
||||
// Manually handle the scroll restoration.
|
||||
history.scrollRestoration = 'manual';
|
||||
|
||||
if (pushState) {
|
||||
history.replaceState({
|
||||
scrollTop: document.scrollingElement.scrollTop,
|
||||
}, '');
|
||||
history.pushState({scrollTop}, '', link);
|
||||
}
|
||||
|
||||
const linkUrl = new URL((link.startsWith('http')) ?
|
||||
link :
|
||||
`${location.origin}${link}`);
|
||||
|
||||
// Fetch new page and switch.
|
||||
const oldHeader = document.querySelector('header');
|
||||
const oldMain = document.querySelector('main');
|
||||
const oldFooter = document.querySelector('footer');
|
||||
const {
|
||||
status,
|
||||
header,
|
||||
main,
|
||||
footer,
|
||||
script: newPageScript,
|
||||
} = await this.fetchFragment(link);
|
||||
|
||||
if (status === 200) {
|
||||
// Replace elements.
|
||||
oldHeader?.parentNode?.replaceChild(header, oldHeader);
|
||||
oldMain?.parentNode?.replaceChild(main, oldMain);
|
||||
oldFooter?.parentNode?.replaceChild(footer, oldFooter);
|
||||
const newScriptFilename = newPageScript?.src.split('/').slice(-1)[0];
|
||||
app.loader.show();
|
||||
try {
|
||||
await app.page?.clear?.();
|
||||
// String manipulation from static analysis.
|
||||
const script = await import(
|
||||
`../js/${newScriptFilename.replace('.js', '')}.js`);
|
||||
// Enable current page re-navigation and avoid double page init.
|
||||
// Check only on url.origin+url.pathname to allow
|
||||
// re-navigation on same page with different url parameters.
|
||||
const prevLinkUrl = new URL(this.previousURL);
|
||||
if (!(app.page instanceof script.default) ||
|
||||
(linkUrl.origin + linkUrl.pathname ===
|
||||
prevLinkUrl.origin + prevLinkUrl.pathname)) {
|
||||
// eslint-disable-next-line new-cap
|
||||
app.page = new script.default();
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
} finally {
|
||||
app.loader.hide();
|
||||
}
|
||||
|
||||
document.scrollingElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// Set to auto in case user hits page refresh.
|
||||
history.scrollRestoration = 'auto';
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch page and get main element;
|
||||
* @param {string} link
|
||||
* @return {*}
|
||||
*/
|
||||
async fetchFragment(link) {
|
||||
const url = (link.startsWith('http')) ? link : `${location.origin}${link}`;
|
||||
link = (new URL(url)).pathname;
|
||||
|
||||
const response = await fetch(link);
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = await response.text();
|
||||
return {
|
||||
status: response.status,
|
||||
header: template.content.querySelector('header')?.cloneNode(true),
|
||||
main: template.content.querySelector('main')?.cloneNode(true),
|
||||
footer: template.content.querySelector('footer')?.cloneNode(true),
|
||||
script: template.content.querySelector('#page-script')?.cloneNode(true),
|
||||
};
|
||||
}
|
||||
|
||||
back() {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
export default Router;
|
47
src/js/routes-page.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import app from './app.js';
|
||||
import pageStyle from '../css/routes-page.css?raw';
|
||||
import AppBar from '../components/app-bar.js';
|
||||
import BottomAppBar from '../components/bottom-app-bar.js';
|
||||
|
||||
class RoutesPage {
|
||||
/**
|
||||
* An object containing useful HTMLElement references.
|
||||
* @type {Object}
|
||||
*/
|
||||
#elements = {};
|
||||
|
||||
constructor() {
|
||||
this.#addPageStyle();
|
||||
this.#setElements();
|
||||
this.#addEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object}
|
||||
*/
|
||||
get elements() {
|
||||
return this.#elements;
|
||||
}
|
||||
|
||||
#addPageStyle() {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(pageStyle);
|
||||
document.adoptedStyleSheets = [...app.styleSheets, sheet];
|
||||
}
|
||||
|
||||
/**
|
||||
* Init `#elements` field that holds element references.
|
||||
*/
|
||||
#setElements() {
|
||||
this.#elements = {};
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
#addEventListeners() {}
|
||||
}
|
||||
|
||||
export default RoutesPage;
|
||||
|
||||
app.init();
|
||||
app.page = new RoutesPage();
|
4
src/js/service-worker.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import {precacheAndRoute} from 'workbox-precaching';
|
||||
|
||||
precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
52
src/pages/home-page.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
<body>
|
||||
<header>
|
||||
<div id="welcome-message">Benvenuti a <span class="bold">Pianello Val Tidone</span></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="route-cards">
|
||||
<div class="route-card">
|
||||
<div class="route-card-left">
|
||||
<img src="../../static/images/app-bar-logo.png">
|
||||
</div>
|
||||
<div class="route-card-center">
|
||||
<div class="name">
|
||||
<div>Percorsi</div>
|
||||
<div class="bold">naturalistici</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="route-card-right"></div>
|
||||
</div>
|
||||
<div class="route-card">
|
||||
<div class="route-card-left">
|
||||
<img src="../../static/images/app-bar-logo.png">
|
||||
</div>
|
||||
<div class="route-card-center">
|
||||
<div class="name">
|
||||
<div>Percorsi</div>
|
||||
<div class="bold">storici</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="route-card-right"></div>
|
||||
</div>
|
||||
<div class="route-card">
|
||||
<div class="route-card-left">
|
||||
<img src="../../static/images/app-bar-logo.png">
|
||||
</div>
|
||||
<div class="route-card-center">
|
||||
<div class="name">
|
||||
<div>Percorsi</div>
|
||||
<div class="bold">tradizionalistici</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="route-card-right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<bottom-app-bar></bottom-app-bar>
|
||||
</footer>
|
||||
|
||||
<script id="page-script" src="../js/home-page.js" type="module"></script>
|
||||
</body>
|
0
src/pages/offline.html
Normal file
28
src/pages/routes-page.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<body>
|
||||
<header>
|
||||
<app-bar></app-bar>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="route-cards">
|
||||
<div class="route-card">
|
||||
<div class="image"></div>
|
||||
<div class="info">
|
||||
<div class="name">
|
||||
<p>Percorso</p>
|
||||
<p class="bold">Pianello n1</p>
|
||||
</div>
|
||||
<div class="duration">
|
||||
Durata <span class="bold">90'</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<bottom-app-bar></bottom-app-bar>
|
||||
</footer>
|
||||
|
||||
<script id="page-script" src="../js/routes-page.js" type="module"></script>
|
||||
</body>
|