Compare commits

..

No commits in common. "main" and "9-search-system" have entirely different histories.

27 changed files with 676 additions and 1915 deletions

View file

@ -1,8 +0,0 @@
# OAuth 2.0 Client ID. Create one at Google Cloud Console > APIs & Services > Credentials
VITE_CLIENT_ID=
# OpenRouteService API key. Create one at OpenRouteService dev dashboard > Tokens
VITE_OPENROUTESERVICE_API_KEY=
# Specify backend URL here
VITE_BACKEND_URL=

3
.gitignore vendored
View file

@ -22,6 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
#env
.env

25
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,25 @@
stages:
- prepare
- build
- deploy
remove-old-services:
stage: prepare
script:
- podman stop little-lines
- podman rm little-lines
container-build:
stage: build
script:
- sed -i "s/DATE/$(date -I)/g" ${CI_PROJECT_DIR}/src/views/About.vue
- sed -i "s/VERSION/$(git log -1 --oneline | awk '{print $1}')/g" ${CI_PROJECT_DIR}/src/views/About.vue
- podman build -t little-lines .
container-deploy:
stage: deploy
script:
- podman run --name little-lines -p 8081:80 -d little-lines
- podman generate systemd little-lines > ~/.config/systemd/user/little-lines.service
- systemctl --user daemon-reload
- systemctl --user enable little-lines

View file

@ -1,23 +0,0 @@
when:
- branch: main
event: push
- event: tag
steps:
- name: deploy
image: node
commands:
- npm i
- npm run build
- rm -rf /mnt/caddy-sites/little-lines.techtransthai.org/*
- cp -r dist/* /mnt/caddy-sites/little-lines.techtransthai.org/
volumes:
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites
environment:
VITE_BACKEND_URL: https://little-lines-backend.techtransthai.org
VITE_CLIENT_ID:
from_secret: client_id
VITE_OPENROUTESERVICE_API_KEY:
from_secret: ors_api_key

View file

@ -1,15 +1,16 @@
FROM docker.io/library/alpine:latest
FROM nginx:alpine
# Set up environment for building
RUN apk add nodejs npm
RUN apk add yarn nodejs
# Copy files to build environment
RUN mkdir /opt/little-lines-frontend
COPY . /opt/little-lines-frontend
# Start the app
WORKDIR /opt/little-lines-frontend
RUN npm i
CMD npm run dev -- --host
RUN mkdir /opt/micromobility-navigation
COPY . /opt/micromobility-navigation
# Run Vite production build
WORKDIR /opt/micromobility-navigation
RUN yarn
RUN yarn run build
# Copy files to nginx path
RUN cp -r dist/* icons /usr/share/nginx/html

View file

@ -6,22 +6,22 @@
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">
<img alt="License: AGPLv3" src="https://shields.io/badge/License-AGPLv3-blueviolet.svg">
</a>
<img alt="Service Status" src="https://status.techtransthai.org/api/badge/7/status">
<a href="https://gitlab.com/openKMITL/micromobility-navigation/-/pipelines">
<img alt="Build Status" src="https://gitlab.com/openkmitl/micromobility-navigation/badges/main/pipeline.svg">
</a>
<h3>ระบบนำทางสำหรับล้อขนาดเล็ก</h3>
<a href="https://little-lines.techtransthai.org">เข้าใช้งานเว็บแอป</a>
&nbsp;&nbsp;
<a href="https://www.techtransthai.org/webapps/little-lines/">โฮมเพจ</a>
<a href="https://gitlab.com/openKMITL/micromobility-navigation/-/wikis/home">เอกสาร</a>
<h5>ร่วมพูดคุยกับเรา:</h5>
<a href="https://t.me/techtransthai">
<img alt="Telegram" src="https://img.shields.io/badge/Telegram-TechTransThai Community-blue">
<a href="https://discord.gg/6aPemyuSzx">
<img alt="Discord" src="https://img.shields.io/discord/1053041544845861015?label=Discord&color=blueviolet">
</a>
</div>
@ -41,8 +41,37 @@ Littles Lines คือระบบนำทางสำหรับล้อข
สำหรับการพัฒนาด้วย NPM
```
$ git clone https://gitlab.com/little-lines/frontend.git
$ cd little-lines
$ git clone https://gitlab.com/openKMITL/micromobility-navigation.git
$ cd micromobility-navigation
$ npm i
$ npm run dev
```
สำหรับการพัฒนาด้วย Yarn
```
$ git clone https://gitlab.com/openKMITL/micromobility-navigation.git
$ cd micromobility-navigation
$ yarn
$ yarn dev
```
สำหรับ Production ด้วย Podman/Docker
```
$ git clone https://gitlab.com/openKMITL/micromobility-navigation.git
$ cd micromobility-navigation
$ podman build -t littlelines:20230720 .
$ podman run --name littlelines -p 8080:80 -d littlelines:20230720
```
## Special Thanks
<div style="display: flex; justify-content: center; align-items: center;">
<a href="https://discord.gg/6aPemyuSzx">
<img src="assets/openKMITL-Community.png" alt=openKMITL" height="40">
<a/>
<a href="https://techtransthai.org">
<img src="assets/ttt-org-wide-grey.svg" alt="TTT Logo" height="45">
<a/>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 1.980469 1.003906 v 14 h 2 v -6 h 2.382812 l 0.722657 1.445313 c 0.171874 0.339843 0.519531 0.554687 0.894531 0.554687 h 5 c 0.554687 0 1 -0.449218 1 -1 v -6 c 0 -0.550781 -0.445313 -1 -1 -1 h -3.378907 l -0.726562 -1.449218 c -0.167969 -0.335938 -0.515625 -0.550782 -0.894531 -0.550782 z m 0 0" fill="#222222"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -580 -600)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -580 -600)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -580 -600)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><path d="m 8 0 c -3.589844 0 -6.5 2.910156 -6.5 6.5 s 2.910156 6.496094 6.5 6.496094 c 3.589844 0.003906 6.5 -2.90625 6.5 -6.496094 s -2.910156 -6.5 -6.5 -6.5 z m 0 4 c 1.378906 0 2.5 1.117188 2.5 2.5 c 0 1.378906 -1.121094 2.5 -2.5 2.496094 c -1.378906 0 -2.5 -1.117188 -2.5 -2.496094 s 1.117188 -2.5 2.5 -2.5 z m 0 0" fill="#222222"/><path d="m 14.097656 8.746094 l -5.660156 0.230468 l -6.535156 -0.230468 c 0.6875 2.152344 4.097656 5.25 6.097656 7.25 v 0.003906 v -0.003906 c 2 -2 5.410156 -5.101563 6.097656 -7.25 z m 0 0" fill="#222222"/><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -700 -60)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -700 -60)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -700 -60)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -320 -60)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -320 -60)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -320 -60)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g><g fill="#222222"><path d="m 2 1 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 12 12 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -12 -12 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0"/><path d="m 5.949219 0.125 c -0.753907 0.019531 -1.433594 0.460938 -1.753907 1.140625 c -0.207031 0.191406 -0.320312 0.457031 -0.320312 0.734375 v 0.5 c 0 0.128906 0.023438 0.253906 0.074219 0.371094 l 4.066406 6.238281 c 0.152344 0.378906 0.519531 0.625 0.929687 0.628906 l 2.234376 1.261719 l 2.046874 3.191406 c 0.144532 0.386719 0.40625 0.808594 0.824219 0.808594 h 0.824219 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -0.804688 l -1.257812 -3.351562 c -0.148438 -0.390626 -0.519531 -0.648438 -0.9375 -0.648438 h -3.324219 l -0.398437 -1 h 2.847656 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -3.648438 l -0.78125 -1.957031 c 0.847657 -0.253907 1.429688 -1.03125 1.429688 -1.917969 c 0 -1.105469 -0.894531 -2 -2 -2 c -0.015625 0 -0.03125 0 -0.046875 0 z m 0 0"/><path d="m 2.855469 6.671875 c -0.261719 0.246094 -0.5 0.519531 -0.707031 0.8125 l 1.398437 1.402344 c 0.179687 -0.320313 0.394531 -0.617188 0.660156 -0.859375 z m -1.273438 1.839844 c -0.164062 0.394531 -0.277343 0.8125 -0.339843 1.25 l 2.207031 2.203125 c -0.210938 -0.441406 -0.339844 -0.929688 -0.339844 -1.453125 c 0 -0.148438 0.027344 -0.289063 0.042969 -0.429688 z m -0.324219 2.851562 c 0.363282 2.234375 2.136719 4.007813 4.375 4.375 z m 7.710938 1.421875 c -0.246094 0.269532 -0.539062 0.484375 -0.855469 0.664063 l 1.371094 1.371093 c 0.289063 -0.210937 0.554687 -0.453124 0.796875 -0.722656 z m -3.9375 0.761719 l 2.199219 2.203125 c 0.4375 -0.066406 0.851562 -0.183594 1.238281 -0.351562 l -1.554688 -1.558594 c -0.144531 0.019531 -0.28125 0.042968 -0.429687 0.042968 c -0.523437 0 -1.015625 -0.125 -1.453125 -0.335937 z m 0 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -360 -60)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -360 -60)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -360 -60)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g><g fill="#222222"><path d="m 4.875 1 c -0.550781 0 -1 0.449219 -1 1 v 0.5 c 0 0.128906 0.023438 0.253906 0.074219 0.371094 l 3 7.5 c 0.148437 0.378906 0.515625 0.628906 0.925781 0.628906 h 3.304688 l 1.257812 3.351562 c 0.148438 0.390626 0.519531 0.648438 0.9375 0.648438 h 1.5 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -0.804688 l -1.257812 -3.351562 c -0.148438 -0.390626 -0.519531 -0.648438 -0.9375 -0.648438 h -3.324219 l -2.675781 -6.691406 v -0.308594 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/><path d="m 8 2.125 c 0 1.105469 -0.894531 2 -2 2 s -2 -0.894531 -2 -2 s 0.894531 -2 2 -2 s 2 0.894531 2 2 z m 0 0"/><path d="m 4.25 5.707031 c -1.808594 0.847657 -3.066406 2.683594 -3.066406 4.800781 c 0 2.917969 2.382812 5.304688 5.300781 5.304688 c 1.933594 0 3.582031 -1.066406 4.488281 -2.628906 l -0.488281 -1.226563 h -0.957031 c -0.539063 1.140625 -1.6875 1.925781 -3.042969 1.925781 c -1.875 0 -3.375 -1.5 -3.375 -3.375 c 0 -1.308593 0.742187 -2.421874 1.824219 -2.984374 z m 0 0" fill-opacity="0.34902"/><path d="m 7 6 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 4 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 0 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -1,2 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="16px" viewBox="0 0 16 16" width="16px"><filter id="a" height="100%" width="100%" x="0%" y="0%"><feColorMatrix color-interpolation-filters="sRGB" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/></filter><mask id="b"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.5"/></g></mask><clipPath id="c"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="d"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.7"/></g></mask><clipPath id="e"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><mask id="f"><g filter="url(#a)"><path d="m -1.6 -1.6 h 19.2 v 19.2 h -19.2 z" fill-opacity="0.35"/></g></mask><clipPath id="g"><path d="m 0 0 h 1600 v 1200 h -1600 z"/></clipPath><g mask="url(#b)"><g clip-path="url(#c)" transform="matrix(1 0 0 1 -340 -60)"><path d="m 550 182 c -0.351562 0.003906 -0.695312 0.101562 -1 0.28125 v 3.4375 c 0.304688 0.179688 0.648438 0.277344 1 0.28125 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 c -0.339844 0 -0.679688 0.058594 -1 0.175781 v 6.824219 h 4 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#d)"><g clip-path="url(#e)" transform="matrix(1 0 0 1 -340 -60)"><path d="m 569 182 v 4 c 1.105469 0 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m 0 5 v 7 h 3 v -4 c 0 -1.65625 -1.34375 -3 -3 -3 z m 0 0"/></g></g><g mask="url(#f)"><g clip-path="url(#g)" transform="matrix(1 0 0 1 -340 -60)"><path d="m 573 182.269531 v 3.449219 c 0.613281 -0.355469 0.996094 -1.007812 1 -1.71875 c 0 -0.714844 -0.382812 -1.375 -1 -1.730469 z m 0 4.90625 v 6.824219 h 2 v -4 c 0 -1.269531 -0.800781 -2.402344 -2 -2.824219 z m 0 0"/></g></g><g fill="#222222"><path d="m 4.875 1 c -0.550781 0 -1 0.449219 -1 1 v 0.5 c 0 0.128906 0.023438 0.253906 0.074219 0.371094 l 3 7.5 c 0.148437 0.378906 0.515625 0.628906 0.925781 0.628906 h 3.304688 l 1.257812 3.351562 c 0.148438 0.390626 0.519531 0.648438 0.9375 0.648438 h 1.5 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -0.804688 l -1.257812 -3.351562 c -0.148438 -0.390626 -0.519531 -0.648438 -0.9375 -0.648438 h -3.324219 l -2.675781 -6.691406 v -0.308594 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/><path d="m 8 2.125 c 0 1.105469 -0.894531 2 -2 2 s -2 -0.894531 -2 -2 s 0.894531 -2 2 -2 s 2 0.894531 2 2 z m 0 0"/><path d="m 4.25 5.707031 c -1.808594 0.847657 -3.066406 2.683594 -3.066406 4.800781 c 0 2.917969 2.382812 5.304688 5.300781 5.304688 c 1.933594 0 3.582031 -1.066406 4.488281 -2.628906 l -0.488281 -1.226563 h -0.957031 c -0.539063 1.140625 -1.6875 1.925781 -3.042969 1.925781 c -1.875 0 -3.375 -1.5 -3.375 -3.375 c 0 -1.308593 0.742187 -2.421874 1.824219 -2.984374 z m 0 0"/><path d="m 7 6 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 4 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 0 0"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 0.0625 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 s 2 -0.894531 2 -2 s -0.894531 -2 -2 -2 z m -1 4.9375 s -1 0 -1 1 v 3 c 0 2 2 2 2 2 h 2 v -0.0625 h 0.371094 l 2.710937 4.515625 c 0.28125 0.472656 0.894531 0.625 1.367188 0.34375 c 0.476562 -0.285156 0.628906 -0.898437 0.34375 -1.375 l -3.289063 -5.484375 h -1.503906 v -0.9375 h 3 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 h -2.585938 l -0.707031 -0.707031 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m -2 1.972656 c -1.613281 0.183594 -3.027344 1.21875 -3.65625 2.742188 c -0.699219 1.679687 -0.308594 3.621094 0.976562 4.90625 c 1.28125 1.28125 3.222657 1.671875 4.902344 0.972656 c 1.523438 -0.628906 2.558594 -2.042969 2.738282 -3.65625 h -2.015626 c -0.164062 0.804688 -0.710937 1.488281 -1.488281 1.8125 c -0.9375 0.386719 -2.007812 0.171875 -2.726562 -0.546875 c -0.714844 -0.714844 -0.929688 -1.785156 -0.542969 -2.722656 c 0.324219 -0.777344 1.007812 -1.324219 1.8125 -1.484375 z m 0 0"/></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

@ -1,14 +0,0 @@
[Unit]
Description=Little Lines frontend container
[Container]
ContainerName=little-lines-frontend
Image=localhost/little-lines-frontend
PublishPort=5173:5173
Volume=/path/to/env:/opt/little-lines-frontend/.env
[Service]
Restart=always
[Install]
WantedBy=multi-user.target default.target

1075
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,21 +9,18 @@
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.2",
"dotenv": "^16.3.1",
"ol": "^9.1.0",
"ol-contextmenu": "^5.4.0",
"axios": "^1.4.0",
"ol": "^7.4.0",
"ol-contextmenu": "^5.2.1",
"ol-ext": "^4.0.10",
"ol-geocoder": "^4.3.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue3-google-login": "^2.0.25",
"vue3-google-oauth2": "^1.0.7",
"vue3-openlayers": "^6.3.0",
"vue3-openlayers": "^1.0.0",
"vuetify": "^3.3.8"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.2.6"
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.0"
}
}

View file

@ -1,432 +1,95 @@
<template>
<v-img
class="image"
src="https://cdn.vuetifyjs.com/images/cards/sunshine.jpg"
cover
></v-img>
<v-card
class="stick-bottom card-height destination-card"
class="stick-bottom card-height"
width="100%"
:height="cardHeight"
style="padding-top: 15px;"
height="60vh"
style="padding-top: 15px; font-weight:bold;"
:title="destination.title"
:subtitle="destination.subTitle"
>
<div v-if="!showRoute">
<v-list>
<v-list-item>
<v-list-item-title class="title-text">{{nearestStructureData.display_name}}</v-list-item-title>
</v-list-item>
<v-list lines="one">
<v-list-item>
<template v-slot:prepend>
<v-avatar>
<img :src="wheelchairIcon" :class="wheelchairIconClass"/>
<img :src="detial[0].icon"
class="iconCheck"
/>
</v-avatar>
</template>
<v-list-item-title :class="wheelchairTextColorClass">{{ wheelchairAccessText }}</v-list-item-title>
<v-list-item-subtitle ><a href="https://wiki.openstreetmap.org/wiki/Key:wheelchair" class="knowMore">เรยนรเพมเต</a></v-list-item-subtitle>
<v-list-item-title class="textCheck">{{detial[0].title}}</v-list-item-title>
<v-list-item-subtitle ><a href="https://wiki.openstreetmap.org/wiki/Key:wheelchair" class="knowMore">{{detial[0].subtitle}}</a></v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-avatar>
<img :src="findLocation"
<img :src="detial[1].icon"
class="icon"
/>
</v-avatar>
</template>
<v-list-item-title class="text-decoration-underline">{{nearestStructureData.lon}} , {{nearestStructureData.lat}}</v-list-item-title>
<v-list-item-title class="text-decoration-underline">{{detial[1].title}}</v-list-item-title>
</v-list-item>
<!-- <v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-avatar>
<img :src="clock"
<img :src="detial[2].icon"
class="icon"
/>
</v-avatar>
</template>
<v-list-item-title class="text-decoration-underline">{{nearestStructureData.infoWheelchair}}</v-list-item-title>
</v-list-item> -->
<v-list-item-title>{{detial[2].title}}</v-list-item-title>
</v-list-item>
</v-list>
<v-card-actions class="stick-bottom btnlist-height justify-sa">
<v-btn @click="addToFavorites" rounded="xl" variant="tonal" width="45vw" height="44px">
<v-btn rounded="xl" variant="tonal" width="45vw" height="44px">
เพมลงในสถานทโปรด</v-btn>
<v-btn @click="viewRoute" rounded="xl" variant="flat" class="text-white" width="45vw" height="44px" color="#f16322">
<v-btn rounded="xl" variant="flat" class="text-white" width="45vw" height="44px" color="#f16322">
เสนทาง</v-btn>
</v-card-actions>
<v-btn :to="{name: 'favorite'}" variant="tonal" icon="mdi-close" style="position: absolute; top: 15px; right: 15px;" />
</div>
<div v-else>
<v-list-item class="d-flex justify-center">
<v-btn-toggle
class="btn-toggle"
mandatory
>
<v-btn class="btn"><v-icon class="icon-walk"></v-icon></v-btn>
<v-btn class="btn"><v-icon class="icon-wheelchair"></v-icon></v-btn>
</v-btn-toggle>
</v-list-item>
<v-list-item class="d-flex justify-center">
<v-toolbar
dense
floating
style="width: 90vw; background-color: transparent;"
>
<v-icon
style="
background-color: transparent;
border-radius: 10px;
width: 12vw;
height: 100%;
">
<v-icon
class="icon-flag"
style="background-color: transparent;"
>
</v-icon>
</v-icon>
<v-text-field
hide-details
prepend-icon="mdi-magnify"
style="
border-radius: 10px;
background-color: rgba(230, 230, 230, 1);
padding-left: 2vw;"
variant="invert-solo"
:model-value="formattedLocation"
flat
></v-text-field>
<v-btn
hide-details
style="
background-color: rgba(230, 230, 230, 1);
margin-left: 2vw;
margin-right: 1vw;
border-radius: 10px;
height: 86%;"
flat
>
<v-icon class="icon-plus"></v-icon>
</v-btn>
</v-toolbar>
</v-list-item>
<v-list-item class="d-flex justify-center"
style="overflow-x:hidden"
>
<v-toolbar
dense
floating
style="
width: 90vw;
background-color: transparent;
flex-wrap: nowrap;
overflow-y: hidden;"
>
<v-icon
style="
background-color: transparent;
border-radius: 10px;
width: 12vw;
height: 100%;
">
<v-icon
class="icon-flag"
style="background-color: rgba(241, 99, 34, 1);"
>
</v-icon>
</v-icon>
<v-text-field
@click="viewPopup"
readonly
hide-details
prepend-icon="mdi-magnify"
style="
border-radius: 10px;
background-color: rgba(230, 230, 230, 1);
padding-left: 2vw;
"
variant="invert-solo"
flat
:model-value="nearestStructureData.display_name"
>
</v-text-field>
<v-btn
hide-details
style="
background-color: rgba(230, 230, 230, 1);
margin-left: 2vw;
margin-right: 1vw;
border-radius: 10px;
height: 86%;
"
flat
>
<v-icon class="icon-vertical-arrows"></v-icon>
</v-btn>
</v-toolbar>
</v-list-item>
<v-list-item class="d-flex justify-center">
<v-card-text>0 นาท</v-card-text>
</v-list-item>
<v-card-actions class="stick-bottom btnlist-height justify-sa">
<v-btn @click="viewPopup" rounded="xl" variant="tonal" width="45vw" height="44px">
อนกล</v-btn>
<v-btn @click="enterRoute" rounded="xl" variant="flat" class="text-white" width="45vw" height="44px" color="#f16322">
เรมการนำทาง</v-btn>
</v-card-actions>
</div>
<v-btn
@click="closePopup"
variant="tonal" icon="mdi-close" style="position: absolute; top: 15px; right: 15px;" />
</v-card>
</template>
<script setup>
import findLocation from '../../icons/Adwaita/find-location.svg';
import clock from '../../icons/Adwaita/clock.svg';
import check from '../../icons/Adwaita/check-round.svg';
import cross from '../../icons/Adwaita/cross.svg';
import wheelchair from '../../icons/Adwaita/wheelchair.svg'
import wheelchairlimited from '../../icons/Adwaita/wheelchair-limited.svg'
import nowheelchair from '../../icons/Adwaita/no-wheelchair.svg'
// import DestinationInfoCard from '@/components/DestinationInfoCard.vue';
// const destination = {
// title: "",
// subTitle: " , "
// }
// const detial = [
// {
// icon: check,
// subtitle: '',
// title: 'Unrestricted Wheelchair access',
// },
// {
// icon: findLocation,
// subtitle: '',
// title: '13.76493, 100.53828',
// },
// {
// icon: clock,
// subtitle: '',
// title: 'Mo-Su 11:30-22:00',
// },
// ]
</script>
<script>
import axios from "axios";
export default {
props: {
nearestStructureData: Object,
onClose: Function,
infoWheelchair: String
},
data() {
return {
showPopup: true,
showRoute: false,
userLocation: null,
isLocationRequested: false,
};
},
computed: {
cardHeight() {
return this.showRoute ? '45vh' : '50vh';
},
formattedLocation() {
if (this.isLocationRequested && !this.userLocation) {
return 'Requesting location...';
} else if (this.userLocation) {
return `${this.userLocation.lon.toFixed(6)}, ${this.userLocation.lat.toFixed(6)}`;
import findLocation from '../../icons/Material/find-location.svg';
import clock from '../../icons/Material/clock.svg';
import check from '../../icons/Material/check-round.svg';
const destination = {
title: "อนุสาวรีย์ชัยสมรภูมิ",
subTitle: "ราชเทวี , กรุงเทพมหานคร"
}
return 'Location not available';
const detial = [
{
icon: check,
subtitle: 'เรียนรู้เพิ่มเติม',
title: 'Unrestricted Wheelchair access',
},
//icon
wheelchairIcon() {
switch(this.nearestStructureData.infoWheelchair) {
case 'yes':
return wheelchair;
case 'limited':
return wheelchairlimited;
case 'no':
return nowheelchair;
default:
return cross;
}
{
icon: findLocation,
subtitle: '',
title: '13.76493, 100.53828',
},
//icon-color
wheelchairIconClass() {
// return this.nearestStructureData.infoWheelchair === 'limited' ? 'iconCheckLimited' : 'iconCheck';
switch(this.nearestStructureData.infoWheelchair) {
case 'yes':
return 'iconCheck';
case 'limited':
return 'iconCheckLimited';
case 'no':
return 'iconCheckNo';
default:
return 'iconCheckDefault';
}
{
icon: clock,
subtitle: '',
title: 'Mo-Su 11:30-22:00',
},
//text
wheelchairAccessText() {
switch(this.nearestStructureData.infoWheelchair) {
case 'yes':
return 'วีลแชร์สามารถเข้าถึงได้';
case 'limited':
return 'วีลแชร์เข้าถึงได้อย่างจำกัด';
case 'no':
return 'วีลแชร์ไม่สามารถเข้าถึงได้';
default:
return 'ไม่มีข้อมูลว่าวีลแชร์สามารถเข้าถึงได้หรือไม่';
}
},
//text-color
wheelchairTextColorClass() {
switch(this.nearestStructureData.infoWheelchair) {
case 'yes':
return 'textColorYes';
case 'limited':
return 'textColorLimited';
case 'no':
return 'textColorNo';
default:
return 'textColorDefault';
}
}
},
methods: {
closePopup() {
this.showPopup = false;
this.$emit('updateRouting', {route:null,isRouting:false});
this.isRouting = false;
this.onClose();
},
viewRoute() {
this.getUserLocation();
this.showRoute = true;
},
enterRoute() {
console.log('Entering Route!');
this.Routing();
},
getUserLocation() {
this.isLocationRequested = true;
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.setLocation, this.handleLocationError)
} else {
console.error("Geolocation is not supported by this browser.");
}
},
setLocation(position) {
this.userLocation = {
lat: position.coords.latitude,
lon: position.coords.longitude
};
console.log('User Location:', this.userLocation);
},
handleLocationError(error) {
console.error('Error getting location:', error);
},
viewPopup(){
this.showPopup = true;
this.showRoute = false;
this.$emit('updateRouting', {route:null,isRouting:false});
this.isRouting = false;
},
addToFavorites() {
const currentUser = JSON.parse(sessionStorage.getItem('current_user'));
if (currentUser) {
console.log('Logged in. Proceed to add to favorites.');
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/favorites/create`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUser.id, // userId
place_name: this.nearestStructureData.display_name,
location: {
type: "Point",
coordinates: [parseFloat(this.nearestStructureData.lon), parseFloat(this.nearestStructureData.lat)]
},
wheelchair_access: 'Accessible',
highway_type: 'Highway'
})
})
.then((res) => {
if (res.ok) {
console.log('Add to favorites success');
return res.json();
} else {
throw Error(`Add to favorites failed (${res.status})`);
}
})
.catch((err) => {
console.log(err);
});
} else {
console.log('User not logged in. Unable to add to favorites.');
this.$router.push({ name: 'login' });
}
},
Routing(){
if(!this.isRouting){
console.log('Start routing!!');
console.log(`nearestStructureData : ${this.nearestStructureData.lon},${this.nearestStructureData.lat}`);
console.log(`userLocation : ${this.userLocation.lon},${this.userLocation.lat}`);
// Make a request to OpenRouteService API for a sample route
const apiKey = import.meta.env.VITE_OPENROUTESERVICE_API_KEY;
const startCoord = `${this.userLocation.lon},${this.userLocation.lat}`;//'100.53860,13.76410';
const endCoord = `${this.nearestStructureData.lon},${this.nearestStructureData.lat}`;//'100.53928,13.76526';
axios.get(`https://api.openrouteservice.org/v2/directions/wheelchair?api_key=${apiKey}&start=${startCoord}&end=${endCoord}`)
.then(response => {
const route = response.data.features[0].geometry.coordinates;
console.log('This is route :',{route:route})
this.$emit('updateRouting', {route:route,isRouting:true});
this.isRouting = true;
})
.catch(error => {
alert(`Can't routing`)
console.error('Error fetching route:', error);
});
}
},
},
};
]
</script>
<style>
@ -466,120 +129,5 @@ export default {
width: 100%;
height: 45vh;
}
.title-text
{
color: rgba(0, 0, 0, 1);
font-style: normal;
font-size: 24px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0px;
text-decoration: none;
text-transform: none;
}
.sutitle-text{
color: rgba(155, 155, 155, 1);
font-style: normal;
font-size: 16px;
font-weight: 400;
line-height: 1.2;
letter-spacing: 0px;
text-decoration: none;
text-transform: none;
}
.icon-walk {
background-color: #000000;
-webkit-mask: url(icons/Adwaita/walk.svg) no-repeat center;
mask: url(icons/Adwaita/walk.svg) no-repeat center;
}
.icon-wheelchair {
background-color: #000000;
-webkit-mask: url(icons/Adwaita/wheelchair.svg) no-repeat center;
mask: url(icons/Adwaita/wheelchair.svg) no-repeat center;
}
.icon-plus {
background-color: #000000;
-webkit-mask: url(icons/Adwaita/plus.svg) no-repeat center;
mask: url(icons/Adwaita/plus.svg) no-repeat center;
}
.icon-vertical-arrows {
background-color: #000000;
-webkit-mask: url(icons/Adwaita/vertical-arrows.svg) no-repeat center;
mask: url(icons/Adwaita/vertical-arrows.svg) no-repeat center;
}
.icon-flag {
background-color: #000000;
-webkit-mask: url(icons/Adwaita/flag.svg) no-repeat center;
mask: url(icons/Adwaita/flag.svg) no-repeat center;
}
.btn-toggle
{
border-radius: 10px;
background-color: rgba(230, 230, 230, 1);
}
.btn-toggle .btn
{
border-radius: 0px;
background-color: rgba(230, 230, 230, 1);
}
.destination-card .v-input__control
{
max-height: 6vh;
max-width: 50vw;
}
.destination-card .v-field__input
{
padding: 0;
}
.iconCheckYes {
width: 30px;
height: 30px;
filter: brightness(0) saturate(100%) invert(45%) sepia(85%) saturate(380%) hue-rotate(100deg) brightness(98%) contrast(87%);
}
.iconCheckLimited {
width: 30px;
height: 30px;
filter: brightness(0) saturate(100%) invert(59%) sepia(84%) saturate(813%) hue-rotate(5deg) brightness(99%) contrast(92%);
}
.iconCheckDefault {
width: 30px;
height: 30px;
filter: brightness(0) saturate(100%) invert(59%) sepia(84%) saturate(813%) hue-rotate(5deg) brightness(99%) contrast(92%);
}
.iconCheckNo {
width: 30px;
height: 30px;
filter: brightness(0) saturate(100%) invert(25%) sepia(20%) saturate(5539%) hue-rotate(326deg) brightness(86%) contrast(109%);
}
.textColorLimited {
filter: brightness(0) saturate(100%) invert(59%) sepia(84%) saturate(813%) hue-rotate(5deg) brightness(99%) contrast(92%);
}
.textColorNo {
filter: brightness(0) saturate(100%) invert(25%) sepia(20%) saturate(5539%) hue-rotate(326deg) brightness(86%) contrast(109%);
}
.textColorDefault {
filter: brightness(0) saturate(100%) invert(59%) sepia(84%) saturate(813%) hue-rotate(5deg) brightness(99%) contrast(92%);
}
.textColorYes {
filter: brightness(0) saturate(100%) invert(45%) sepia(85%) saturate(380%) hue-rotate(100deg) brightness(98%) contrast(87%);
}
</style>

View file

@ -1,38 +0,0 @@
<template>
<v-card
class="stick-bottom card-height"
width="100%"
height="60vh"
style="padding-top: 15px; font-weight:bold;"
:title="nearestStructureData.display_name"
:subtitle="destination.subTitle"
>
</v-card>
</template>
<script>
export default {
props: {
nearestStructureData: Object,
onClose: Function,
},
data() {
return {
showPopup: true,
};
},
methods: {
closePopup() {
this.showPopup = false;
this.onClose();
},
viewRoute() {
this.$emit('changeComponent', { userLocation: this.userLocation, destination: this.destination });
},
},
};
</script>

View file

@ -8,9 +8,6 @@
</template>
<script>
import DestinationInfoCard from '@/components/DestinationInfoCard.vue';
export default {
props: {
nearestStructureData: Object,

View file

@ -1,7 +1,6 @@
import { createApp } from 'vue'
import '@/style.css'
// Vuetify
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
@ -13,11 +12,10 @@ import OpenLayersMap from "vue3-openlayers";
import App from '@/App.vue'
import router from '@/plugins/router'
import vue3GoogleLogin from 'vue3-google-login'
const vuetify = createVuetify({
components,
directives,
})
createApp(App).use(router).use(vuetify).use(OpenLayersMap).use(vue3GoogleLogin,{ clientId: import.meta.env.VITE_CLIENT_ID }).mount('#app')
createApp(App).use(router).use(vuetify).use(OpenLayersMap).mount('#app')

View file

@ -1,88 +1,32 @@
<template>
<v-sheet class="d-flex justify-center align-center">
<v-sheet class="d-flex justify-center">
<v-sheet class="ma-2 pa-4 mb-auto">
<v-sheet style="display: flex; align-items: center;justify-content: center;">
<v-row class="ma-2" justify="centered">
<img src="../../icons/LittleLines.svg">
</v-sheet>
<p class="text-center text-h4 font-weight-black">Little Lines</p>
<p class="text-center">TechTransThai Community</p>
<v-sheet style="display: flex; align-items: center;justify-content: center;">
<a class="versionbutton">2024.06.0</a>
</v-sheet>
<v-list class = "ma-2" density="compact" max-width="420px" min-width="200px" width="95vw">
<v-list-item
v-for="(item, i) in sourcecode_items"
:key="i"
:value="item"
:href="'https://forge.techtransthai.org/little-lines'"
>
<template v-slot:append>
<!-- <v-icon :icon="item.icon"></v-icon> -->
<img src="../../icons/Adwaita/right.svg">
</template>
<v-list-item-title v-text="item.text"></v-list-item-title>
</v-list-item>
</v-list>
<v-list class = "ma-2" density="compact">
<v-list-subheader>รายงานปญหาและขอเสนอแนะ</v-list-subheader>
<v-img cover src="../../icons/LittleLines.svg"></v-img>
</v-row>
<v-list-item
v-for="(item, i) in report_items"
:key="i"
:value="item"
:href="item.link"
center
class="text-black"
title="Little Lines"
subtitle="openKMITL Community"
>
<template v-slot:append>
<!-- <v-icon :icon="item.icon"></v-icon> -->
<img src="../../icons/Adwaita/right.svg">
</template>
<v-list-item-title v-text="item.text"></v-list-item-title>
</v-list-item>
</v-list>
<a class="versionbutton">DATE-VERSION</a>
</v-sheet>
</v-sheet>
</template>
<script setup>
import { getTopRight } from 'ol/extent';
import {RouterLink} from 'vue-router';
const sourcecode_items = [
{ text: 'ซอร์สโค้ด',
icon: 'mdi-chevron-right',}
]
const report_items = [
{ text: 'Discord',
icon: 'mdi-chevron-right',
link: 'https://discord.gg/3tRdRE3tGv'},
{ text: 'Facebook',
icon: 'mdi-chevron-right',
link: 'https://www.facebook.com'},
{ text: 'Google Forms',
icon: 'mdi-chevron-right',
link: 'https://forms.google.com'},
]
</script>
<style>
@ -101,11 +45,6 @@ import {RouterLink} from 'vue-router';
padding-left: 15px;
margin-top: 10px;
margin-bottom: 10px;
width: 200px;
}
.text-h4 {
font-family: Cantarell, sans-serif !important;
}
</style>

View file

@ -1,83 +1,79 @@
<template>
<v-card
class="mx-auto fav-card">
<!-- <h1>Favorite</h1> -->
<!-- <v-card
class="mx-auto"
max-width="420"
height="600"
>
<v-list lines="two">
<v-list-item
v-for="(favorite, index) in favorite"
:key="index"
@click="handleClick(favorite)"
>
<template v-slot:prepend>
<v-icon class="icon-pin"></v-icon>
</template>
<v-list-item-content>
<v-list-item-title>{{ favorite.place_name }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
v-for="item in items"
:key="item.title"
:title="item.title"
:subtitle="item.desc"
:prepend-avatar="pin_svg"
></v-list-item>
</v-list>
</v-card>
</v-card> -->
<v-card fluid
class="mx-auto"
max-width="500px"
width="90vw"
max-height="600px"
height="90vh"
>
<v-list
item-props
lines="three"
>
<v-list-item
v-for="(item, i) in items"
:to="{name: 'destination-info'}"
:key="i"
:value="item"
>
<template v-slot:prepend>
<v-avatar>
<v-img
:height="30"
src= "./icons/Adwaita/pin.svg"
></v-img>
</v-avatar>
</template>
<script>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
<v-list-item-title v-text="item.title"></v-list-item-title>
<v-list-item-subtitle v-text="item.desc"></v-list-item-subtitle>
<v-divider></v-divider>
</v-list-item>
</v-list>
</v-card>
export default {
setup() {
const favorite = ref([]);
const router = useRouter();
const handleClick = (favoriteItem) => {
console.log('Clicked:', favoriteItem);
router.push({ name: 'home', params: { favoriteLocation: favoriteItem } });
</template>
};
<script setup>
import {RouterLink} from 'vue-router';
onMounted(async () => {
try {
const currentUser = JSON.parse(sessionStorage.getItem('current_user'));
if (currentUser && currentUser.id) {
const response = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/favorites/${currentUser.id}`);
if (!response.ok) {
throw new Error('Failed to fetch favorites');
}
const data = await response.json();
favorite.value = data;
}
} catch (error) {
console.error(error);
}
});
return {
favorite,
handleClick
};
}
};
const pin_svg = "./icons/Adwaita/pin.svg"
const items = [
{
title: 'หอพักนักศึกษา',
desc: 'ลาดกระบัง, กรุงเทพมหานคร',
},
{
title: 'อนุสาวรีย์ชัยสมรภูมิ',
desc: 'ราชเทวี, กรุงเทพมหานคร',
},
]
</script>
<style >
.icon-pin{
background-color: black;
-webkit-mask: url(icons/Adwaita/pin.svg) no-repeat center;
mask: url(icons/Adwaita/pin.svg) no-repeat center;
}
.fav-card
{
margin-top: 2vh;
max-width: 90vw;
border-radius: 12px;
background-color: rgba(255, 255, 255, 1);
box-shadow: 0px 1px 4px 1px rgba(0, 0, 0, 0.13);
}
</style>

View file

@ -1,18 +1,12 @@
<template>
<div id="app">
<router-view
class="router-view"
/>
<DestinationInfoCard
class="DestinationInfoCard"
<router-view />
<Popup
v-if="popupData"
:nearestStructureData="popupData"
:onClose="closePopup"
@updateRouting="handleRouting"
/>
</div>
<!-- <searchbar/> -->
@ -20,43 +14,30 @@
<v-app-bar scroll-threshold="0"
class="mx-auto px-auto"
>
<div class="flex-grow-1">
<v-text-field
v-model="searchQuery"
@keyup.enter="performSearch"
@input="performSearch"
@click="toggleSearchBar()"
density="compact"
variant="solo"
prepend-inner-icon="mdi-magnify"
single-line
hide-details
></v-text-field>
</div>
<v-btn icon @click="showSearchBar = !showSearchBar">
<v-btn icon>
<v-icon>mdi-crosshairs-gps</v-icon>
</v-btn>
</v-app-bar>
</v-layout>
<v-list v-if="searchResults.length > 0 && !showSearchBar" class="search-results">
<v-list-item
v-for="result in searchResults"
:key="result.place_id"
@click="moveToLocation(result),toggleSearchBar()"
>
<v-list-item-icon>
<v-icon>mdi-map-marker</v-icon>
</v-list-item-icon>
<div v-if="searchResults.length > 0" class="search-results">
<ul>
<li v-for="result in searchResults" :key="result.place_id" @click="moveToLocation(result)">
{{ result.display_name }}
</v-list-item>
</v-list>
</li>
</ul>
</div>
<!-- <map/> -->
<ol-map
:loadTilesWhileAnimating="true"
@ -80,33 +61,6 @@
<ol-tile-layer>
<ol-source-osm />
</ol-tile-layer>
<ol-vector-layer v-if="isRouting">
<ol-source-vector>
<!-- Line String Geometry -->
<ol-feature>
<ol-geom-line-string :coordinates="route"></ol-geom-line-string>
</ol-feature>
<!-- Multi Point Geometry -->
<ol-feature>
<ol-geom-multi-point :coordinates="[
[route[0][0],route[0][1]],
[route[route.length-1][0],route[route.length-1][1]]
]"></ol-geom-multi-point>
</ol-feature>
</ol-source-vector>
<!-- Style for the Line and Points -->
<ol-style>
<!-- Style for Line -->
<ol-style-stroke :color="strokeColor" :width="strokeWidth"></ol-style-stroke>
<!-- Style for Points -->
<ol-style-icon :src="startMarker" :scale="0.1" :anchor="[0.5, 1]"></ol-style-icon>
</ol-style>
</ol-vector-layer>
</ol-map>
</template>
@ -114,12 +68,11 @@
<script setup>
import searchbar from '@/components/searchbar.vue';
import Popup from "@/components/Popup.vue"; // Import the Popup componen
import { ref } from "vue";
import axios from "axios";
import DestinationInfoCard from '@/components/DestinationInfoCard.vue';
import startMarker from '../../assets/map-marker-symbolic.png';
import stopMarker from '../../assets/flag-filled-symbolic.png';
const center = ref([100.538611, 13.764722]);
const projection = ref("EPSG:4326");
@ -128,27 +81,17 @@ const rotation = ref(0);
const popupData = ref(null);
const isRouting = ref(false);
const route = ref(null);
const infoWheelchair = ref(null)
const strokeWidth = ref(5);
const strokeColor = ref("red");
//search
const searchQuery = ref("");
const searchResults = ref([]);
const showSearchBar = ref(false);
const toggleSearchBar = () => {
showSearchBar.value = !showSearchBar.value;
};
const moveToLocation = (result) => {
// Extract latitude and longitude from the selected result
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
// Update the center coordinates to move the camera to the selected location
center.value = [lon, lat];
showSearchBar.value = false;
popupData.value = result;
};
const performSearch = async () => {
@ -157,56 +100,26 @@ const performSearch = async () => {
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery.value)}`
);
// Process the search results and limit to, let's say, 5 results
searchResults.value = response.data.slice(0, 5);
} catch (error) {
console.error("Error fetching search results:", error);
}
};
//Show API
const handleMapClick = async event => {
const clickedCoordinate = event.coordinate;
const overpassQuery = `[out:json];
(
node(around:10,${clickedCoordinate[1]},${clickedCoordinate[0]})["wheelchair"];
way(around:10,${clickedCoordinate[1]},${clickedCoordinate[0]})["wheelchair"];
relation(around:10,${clickedCoordinate[1]},${clickedCoordinate[0]})["wheelchair"];
);
out;`;
const overpassUrl = 'https://overpass-api.de/api/interpreter';
axios.post(overpassUrl, `data=${encodeURIComponent(overpassQuery)}`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then(response => {
// Process the data returned by the Overpass API
response.data.elements.forEach(element => {
if (element.tags && element.tags.wheelchair) {
console.log(`wheelchair: ${element.tags.wheelchair}`);
const wheelchairValues = element.tags.wheelchair;
infoWheelchair.value = wheelchairValues;
}
})
}).catch(error => {
console.error('Error fetching data from Overpass API:', error);
});
try {
const response = await axios.get(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${clickedCoordinate[1]}&lon=${clickedCoordinate[0]}`
);
let nearestStructureData = response.data;
nearestStructureData = {
...nearestStructureData,
infoWheelchair: infoWheelchair
}
//infoWheelchair can null,yes,no
const nearestStructureData = response.data;
popupData.value = nearestStructureData; // Show popup
// console.log(nearestStructureData)
} catch (error) {
console.error("Error fetching reverse geocoding data:", error);
}
@ -216,14 +129,6 @@ const closePopup = () => {
popupData.value = null; // Hide popup
};
const handleRouting = (res) => {
console.log("Received Route:", res);
route.value = res.route;
isRouting.value = res.isRouting;
};
</script>
<style>
@ -261,12 +166,4 @@ const handleRouting = (res) => {
.search-results li:hover {
background-color: #f0f0f0;
}
.router-view{
z-index: -15;
}
.DestinationInfoCard{
z-index: 15;
}
</style>

View file

@ -3,82 +3,31 @@
<v-app>
<top-bar :show-back-icon="true" :page-title="pageTitle" />
<v-main>
<div class="text-center mt-8 mb-16">
<div class="text-h4 font-weight-bold">
นดอนรบกลบส
<div>Little Lines</div>
<v-container>
<v-form name="login-form">
<div class="mb-3">
<label for="username">Username: </label>
<input type="text" id="username" v-model="input.username" />
</div>
<div class="mb-3">
<label for="password">Password: </label>
<input type="password" id="password" v-model="input.password" />
</div>
<div class="mx-5">
<v-text-field
class="email"
v-model="input.email"
label="อีเมล"
variant="solo"
>
<template v-slot:append-inner>
<img
class="iconEdit"
:src="edit"
>
</template>
</v-text-field>
<v-text-field
class="password"
v-model="input.password"
label="รหัสผ่าน"
variant="solo"
>
<template v-slot:append-inner>
<img
class="iconEdit"
:src="edit"
>
<img
class="iconEyeNotLooking"
:src="eyeNotLooking"
>
</template>
</v-text-field>
</div>
<v-contaioner>
<v-row class="button">
<v-btn @click.prevent="login" rounded="xl" variant="flat" class="text-white" width="45vw" height="44px" color="#f16322">
ลงชอเขาใช</v-btn>
</v-row>
<v-row class="button">
<v-btn @click.prevent="loginGoogle" class="text-none" rounded="xl" variant="tonal" width="45vw" height="44px">
ลงชอเขาใชวย Google</v-btn>
</v-row>
<v-row class="button">
<v-btn :to="{name: 'register'}" rounded="xl" variant="tonal" width="45vw" height="44px">
นตองการสมครสมาช</v-btn>
</v-row>
</v-contaioner>
</v-form>
</v-container>
<v-btn @click.prevent="login">ลงชอเขาใช</v-btn>
<v-btn :to="{name: 'register'}">นตองการสมครสมาช</v-btn>
</v-main>
</v-app>
</template>
<script setup>
import edit from '../../icons/Material/edit.svg';
import eyeNotLooking from '../../icons/Material/eye-not-looking.svg';
</script>
<script>
import {RouterLink} from 'vue-router';
import TopBar from '@/components/TopBar.vue';
import { VContainer } from 'vuetify/lib/components/index.mjs';
import { googleSdkLoaded } from "vue3-google-login";
import axios from 'axios'
export default {
components: {
@ -89,107 +38,20 @@ export default {
return {
pageTitle: 'ลงชื่อเข้าใช้',
input: {
email: '',
username: '',
password: ''
}
};
},
methods: {
login() {
if (this.input.email !== '' && this.input.password !== '') {
if (this.input.username !== '' || this.input.password !== '') {
console.log('Authenticated: Checking with Backend');
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/users/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: this.input.email,
password: this.input.password,
}),
})
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw Error(`Login failed (${res.status})`);
}
})
.then((data) => {
console.log(data.success);
sessionStorage.setItem('current_user', JSON.stringify({id:data.user._id,username:data.user.username,email:data.user.email}));
this.$router.push({ name: 'home' });
})
.catch((err) => {
console.log(err);
// Handle the error, e.g., display an error message to the user
});
} else {
console.log('Email and Password cannot be empty');
}
},
loginGoogle(){
googleSdkLoaded(google => {
google.accounts.oauth2
.initCodeClient({
client_id:
import.meta.env.VITE_CLIENT_ID,
scope: "email profile openid",
redirect_uri: `${import.meta.env.VITE_BACKEND_URL}/api/users/googleAuth/callback`,
callback: response => {
if (response.code) {
this.sendCodeToBackend(response.code);
}
}
})
.requestCode();
});
},
async sendCodeToBackend(code) {
try {
const headers = {
Authorization: code
};
const response = await axios.post(`${import.meta.env.VITE_BACKEND_URL}/api/users/googleAuth`, null, { headers });
const userDetails = response.data;
console.log("User Details:", userDetails);
this.userDetails = userDetails;
sessionStorage.setItem('current_user', JSON.stringify({id:userDetails.user._id}));
this.$router.push({ name: 'home' });
// Redirect to the homepage ("/")
// this.$router.push({ name: 'home' });
} catch (error) {
console.error("Failed to send authorization code:", error);
console.log('Username and Password cannot be empty');
}
}
}
};
</script>
<style>
.username {
margin-bottom: -21px;
}
.iconEdit, .iconEyeNotLooking {
width: 25px;
height: 25px;
}
.iconEdit {
margin-right: 10px;
}
.iconEyeNotLooking {
margin-right: 10px;
margin-left: 10px;
}
.button {
padding: 10px;
display: flex;
justify-content: center;
align-content: center;
}
</style>

View file

@ -3,89 +3,31 @@
<v-app>
<top-bar :show-back-icon="true" :page-title="pageTitle" />
<v-main>
<div class="text-center mt-8 mb-16">
<div class="text-h4">
<div class="font-weight-bold">Little Lines</div>
<v-container>
<v-form name="register-form">
<div class="mb-3">
<label for="username">Username: </label>
<input type="text" id="username" v-model="input.username" />
</div>
<div>ระบบนำทางสำหร Micromobility</div>
<div class="mb-3">
<label for="password">Password: </label>
<input type="password" id="password" v-model="input.password" />
</div>
<div class="mb-3">
<label for="password">Password Confirmation: </label>
<input type="password" id="passwordConfirm" v-model="input.passwordConfirm" />
</div>
<div class="mx-5">
<v-text-field
class="email"
v-model="input.email"
label="อีเมล"
variant="solo"
>
<template v-slot:append-inner>
<img
class="iconEdit"
:src="edit"
>
</template>
</v-text-field>
<v-text-field
class="password"
v-model="input.password"
label="รหัสผ่าน"
variant="solo"
>
<template v-slot:append-inner>
<img
class="iconEdit"
:src="edit"
>
<img
class="iconEyeNotLooking"
:src="eyeNotLooking"
>
</template>
</v-text-field>
<v-text-field
class="passwordC"
v-model="input.passwordConfirm"
label="ยืนยันรหัสผ่าน"
variant="solo"
>
<template v-slot:append-inner>
<img
class="iconEdit"
:src="edit"
>
<img
class="iconEyeNotLooking"
:src="eyeNotLooking"
>
</template>
</v-text-field>
</div>
<v-checkbox color="#F16322" class="check">
<template v-slot:label>
<div>นไดานและยอมร</div>
<a href="https://www.google.co.th/?hl=th" >นโยบายความเปนสวนต</a>
</template>
</v-checkbox>
<div class="button-register">
<v-btn @click.prevent="register" rounded="xl" variant="flat" class="text-white" width="45vw" height="44px" color="#f16322">
สมครสมาช</v-btn>
</div>
</v-form>
</v-container>
<v-btn @click.prevent="login">สมครสมาช</v-btn>
</v-main>
</v-app>
</template>
<script setup>
import edit from '../../icons/Material/edit.svg';
import eyeNotLooking from '../../icons/Material/eye-not-looking.svg';
</script>
<script>
import TopBar from '@/components/TopBar.vue';
@ -96,84 +38,24 @@ import eyeNotLooking from '../../icons/Material/eye-not-looking.svg';
name: 'register',
data() {
return {
pageTitle: 'สมัครสมาชิ',
pageTitle: 'สมัครสมาชิ',
input: {
email: '',
username: '',
password: '',
passwordConfirm:''
}
};
},
methods: {
register() {
if (this.input.username !== '' && ((this.input.password !='') && (this.input.password == this.input.passwordConfirm))) {
login() {
if (this.input.username !== '' || this.input.password !== '') {
console.log('Authenticated: Checking with Backend');
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/users/create`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: "temp",
password: this.input.password,
email: this.input.email,
isGoogleAccount: false
})
})
.then((res) => {
if(res.ok){
return res.json()
}
else{
return res.json().then(data => {throw Error(`${data.registerStatus}`) });
}
})
.then((data) => {
console.log(data.registerStatus)
this.$router.push({name : 'login'})
})
.catch((err) =>{
console.log(err)
})
console.log("fisnished fetch");
} else {
console.log('Email and Password cannot be empty');
console.log('Username and Password cannot be empty');
}
}
}
},
};
</script>
<style>
.user {
background-color: aqua;
}
.email, .password {
margin-bottom: -21px;
}
.check {
display: flex;
justify-content:center;
padding-top: 5%;
padding-bottom: 5%;
}
.button-register {
margin: 0;
position: absolute;
left: 50%;
-ms-transform: translateX(-50%);
transform: translateX(-50%);
}
.iconEdit, .iconEyeNotLooking {
width: 25px;
height: 25px;
}
.iconEdit {
margin-right: 10px;
}
.iconEyeNotLooking {
margin-right: 10px;
margin-left: 10px;
}
</style>

View file

@ -1,12 +1,12 @@
<template>
<v-app>
<v-container class="d-flex justify-center align-center">
<div v-if="!currentUser">
<div>
<h1>งไมไดลงชอเขาใช</h1>
<h2>ลงชอเขาใชเพอบนทกการตงคาอยางปลอดภ</h2>
<v-container class="d-flex justify-center align-center">
<v-btn class="ma-2" width="150" :to="{name: 'login'}">ลงชอเขาใช</v-btn>
<v-btn class="ma-2" width="150" :to="{name: 'register'}">สมครสมาช</v-btn>
<v-btn class="ma-2" width="150" :to="{name: 'register'}">สมครสมาช</v-btn>
</v-container>
</div>
</v-container>
@ -55,7 +55,7 @@
</v-list-item>
</v-list>
<v-btn @click.prevent="logout" class = "ma-2 mt-3" width="100%"
<v-btn class = "ma-2 mt-3" width="100%"
color="red">
ลงชอออก
</v-btn>
@ -69,10 +69,6 @@
<script setup>
import {RouterLink} from 'vue-router';
import { VContainer } from 'vuetify/lib/components/index.mjs';
import router from '@/plugins/router'
const currentUser = sessionStorage.getItem('current_user');
const account_items = [
{ text: 'ตั้งค่าบัญชี',
@ -94,18 +90,5 @@ const display_items = [
icon: 'mdi-chevron-right',}
]
function logout() {
if (sessionStorage.getItem('current_user')){
console.log('loging out...');
fetch(`${import.meta.env.VITE_BACKEND_URL}/api/users/logout`, {
method: "GET"
}).then(()=>{
console.log('loged out');
sessionStorage.removeItem('current_user');
router.push({ name: 'home' });
})
}
}
</script>