adds komoot integration
This commit is contained in:
371
db/integrations/komoot/komoot.go
Normal file
371
db/integrations/komoot/komoot.go
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
package komoot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/forms"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
|
"github.com/twpayne/go-gpx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SyncKomoot(app *pocketbase.PocketBase) error {
|
||||||
|
integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("true"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range integrations {
|
||||||
|
userId := i.GetString("user")
|
||||||
|
komootString := i.GetString("komoot")
|
||||||
|
var komootIntegration KomootIntegration
|
||||||
|
json.Unmarshal([]byte(komootString), &komootIntegration)
|
||||||
|
|
||||||
|
if !komootIntegration.Active || komootIntegration.Email == "" || komootIntegration.Password == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k := &KomootApi{}
|
||||||
|
err = k.login(komootIntegration.Email, komootIntegration.Password)
|
||||||
|
if err != nil {
|
||||||
|
warning := fmt.Sprintf("komoot login failed: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasNewTours := true
|
||||||
|
page := 0
|
||||||
|
for hasNewTours {
|
||||||
|
tours, err := k.fetchTours(page)
|
||||||
|
if err != nil {
|
||||||
|
warning := fmt.Sprintf("error fetching tours from komoot: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNewTours, err = syncTrailWithTours(app, k, userId, tours)
|
||||||
|
if err != nil {
|
||||||
|
warning := fmt.Sprintf("error syncing komoot tours with trails: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasicAuthToken struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b BasicAuthToken) Apply(req *http.Request) {
|
||||||
|
authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(b.Key+":"+b.Value))
|
||||||
|
req.Header.Set("Authorization", authStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type KomootApi struct {
|
||||||
|
UserID string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KomootApi) buildHeader() *BasicAuthToken {
|
||||||
|
if k.UserID != "" && k.Token != "" {
|
||||||
|
return &BasicAuthToken{k.UserID, k.Token}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendRequest(url string, auth *BasicAuthToken) ([]byte, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth != nil {
|
||||||
|
auth.Apply(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("error sending request to komoot (%d): %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KomootApi) login(email, password string) error {
|
||||||
|
url := fmt.Sprintf("https://api.komoot.de/v006/account/email/%s/", email)
|
||||||
|
|
||||||
|
body, err := sendRequest(url, &BasicAuthToken{email, password})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data LoginResponse
|
||||||
|
json.Unmarshal(body, &data)
|
||||||
|
|
||||||
|
k.UserID = data.Username
|
||||||
|
k.Token = data.Password
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (k *KomootApi) fetchTours(page int) ([]KomootTour, error) {
|
||||||
|
currentUri := fmt.Sprintf("https://api.komoot.de/v007/users/%s/tours/?page=%d&sort_field=date&sort_direction=desc&limit=30", k.UserID, page)
|
||||||
|
|
||||||
|
body, err := sendRequest(currentUri, k.buildHeader())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data KomootToursResponse
|
||||||
|
json.Unmarshal(body, &data)
|
||||||
|
|
||||||
|
tours := data.Embedded.Tours
|
||||||
|
|
||||||
|
return tours, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *KomootApi) fetchDetailedTour(tour KomootTour) (*DetailedKomootTour, error) {
|
||||||
|
url := fmt.Sprintf("https://api.komoot.de/v007/tours/%d?_embedded=coordinates,way_types,surfaces,directions,participants,timeline&directions=v2&fields=timeline&format=coordinate_array&timeline_highlights_fields=tips,recommenders", tour.ID)
|
||||||
|
body, err := sendRequest(url, k.buildHeader())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data *DetailedKomootTour
|
||||||
|
json.Unmarshal(body, &data)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncTrailWithTours(app *pocketbase.PocketBase, k *KomootApi, user string, tours []KomootTour) (bool, error) {
|
||||||
|
hasNewTours := false
|
||||||
|
for _, tour := range tours {
|
||||||
|
trails, err := app.Dao().FindRecordsByFilter("trails", "external_id = {:id}", "", 1, 0, dbx.Params{"id": strconv.Itoa(int(tour.ID))})
|
||||||
|
if err != nil {
|
||||||
|
return hasNewTours, err
|
||||||
|
}
|
||||||
|
if len(trails) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasNewTours = true
|
||||||
|
detailedTour, err := k.fetchDetailedTour(tour)
|
||||||
|
if err != nil {
|
||||||
|
return hasNewTours, err
|
||||||
|
}
|
||||||
|
gpx, err := generateTourGPX(detailedTour)
|
||||||
|
if err != nil {
|
||||||
|
return hasNewTours, err
|
||||||
|
}
|
||||||
|
wpIds, err := createWaypointsFromTour(app, detailedTour, user)
|
||||||
|
if err != nil {
|
||||||
|
return hasNewTours, err
|
||||||
|
}
|
||||||
|
err = createTrailFromTour(app, detailedTour, gpx, user, wpIds)
|
||||||
|
if err != nil {
|
||||||
|
return hasNewTours, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return hasNewTours, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTrailFromTour(app *pocketbase.PocketBase, detailedTour *DetailedKomootTour, gpx *filesystem.File, user string, wpIds []string) error {
|
||||||
|
collection, err := app.Dao().FindCollectionByNameOrId("trails")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
record := models.NewRecord(collection)
|
||||||
|
|
||||||
|
form := forms.NewRecordUpsert(app, record)
|
||||||
|
|
||||||
|
categoryMap := map[string]string{
|
||||||
|
"hike": "Hiking",
|
||||||
|
"touringbicycle": "Biking",
|
||||||
|
"mtb": "Biking",
|
||||||
|
"racebike": "Biking",
|
||||||
|
"jogging": "Walking",
|
||||||
|
"mtb_easy": "Workout",
|
||||||
|
"mtb_advanced": "Walking",
|
||||||
|
"mountaineering": "Hiking",
|
||||||
|
}
|
||||||
|
|
||||||
|
category, _ := app.Dao().FindFirstRecordByData("categories", "name", categoryMap[detailedTour.Sport])
|
||||||
|
categoryId := ""
|
||||||
|
if category != nil {
|
||||||
|
categoryId = category.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
photo, err := fetchPhoto(detailedTour.MapImage.Src, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
form.LoadData(map[string]any{
|
||||||
|
"name": detailedTour.Name,
|
||||||
|
"public": detailedTour.Status == "public",
|
||||||
|
"distance": detailedTour.Distance,
|
||||||
|
"elevation_gain": detailedTour.ElevationUp,
|
||||||
|
"elevation_loss": detailedTour.ElevationDown,
|
||||||
|
"duration": detailedTour.Duration / 60,
|
||||||
|
"date": detailedTour.Date,
|
||||||
|
"external_provider": "komoot",
|
||||||
|
"external_id": strconv.Itoa(detailedTour.ID),
|
||||||
|
"lat": detailedTour.StartPoint.Lat,
|
||||||
|
"lon": detailedTour.StartPoint.Lng,
|
||||||
|
"difficulty": detailedTour.Difficulty.Grade,
|
||||||
|
"category": categoryId,
|
||||||
|
"waypoints": wpIds,
|
||||||
|
"author": user,
|
||||||
|
})
|
||||||
|
|
||||||
|
form.AddFiles("photos", photo)
|
||||||
|
form.AddFiles("gpx", gpx)
|
||||||
|
|
||||||
|
if err := form.Submit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createWaypointsFromTour(app *pocketbase.PocketBase, tour *DetailedKomootTour, user string) ([]string, error) {
|
||||||
|
collection, err := app.Dao().FindCollectionByNameOrId("waypoints")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wpIds := make([]string, len(tour.Embedded.Timeline.Embedded.Items))
|
||||||
|
|
||||||
|
for i, wp := range tour.Embedded.Timeline.Embedded.Items {
|
||||||
|
photos, err := fetchWaypointPhotos(wp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
record := models.NewRecord(collection)
|
||||||
|
form := forms.NewRecordUpsert(app, record)
|
||||||
|
|
||||||
|
wpDescription := ""
|
||||||
|
if len(wp.Embedded.Reference.Embedded.Tips.Embedded.Items) > 0 {
|
||||||
|
wpDescription = wp.Embedded.Reference.Embedded.Tips.Embedded.Items[0].Text
|
||||||
|
}
|
||||||
|
|
||||||
|
wpLat := wp.Embedded.Reference.StartPoint.Lat
|
||||||
|
if wpLat == 0 {
|
||||||
|
wpLat = tour.StartPoint.Lat
|
||||||
|
}
|
||||||
|
|
||||||
|
wpLon := wp.Embedded.Reference.StartPoint.Lng
|
||||||
|
if wpLon == 0 {
|
||||||
|
wpLon = tour.StartPoint.Lng
|
||||||
|
}
|
||||||
|
|
||||||
|
form.LoadData(map[string]any{
|
||||||
|
"name": wp.Embedded.Reference.Name,
|
||||||
|
"description": wpDescription,
|
||||||
|
"lat": wpLat,
|
||||||
|
"lon": wpLon,
|
||||||
|
"icon": "circle",
|
||||||
|
"author": user,
|
||||||
|
"distance_from_start": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, photo := range photos {
|
||||||
|
form.AddFiles("photos", photo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := form.Submit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wpIds[i] = record.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return wpIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWaypointPhotos(wp Item) ([]*filesystem.File, error) {
|
||||||
|
|
||||||
|
photos := make([]*filesystem.File, len(wp.Embedded.Reference.Embedded.Images.Embedded.Items))
|
||||||
|
|
||||||
|
for i, img := range wp.Embedded.Reference.Embedded.Images.Embedded.Items {
|
||||||
|
photo, err := fetchPhoto(img.Src, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
photos[i] = photo
|
||||||
|
}
|
||||||
|
|
||||||
|
return photos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPhoto(url string, width string, height string) (*filesystem.File, error) {
|
||||||
|
url = strings.Replace(url, "{crop}", "false", 1)
|
||||||
|
url = strings.Replace(url, "{width}", width, 1)
|
||||||
|
url = strings.Replace(url, "{height}", height, 1)
|
||||||
|
|
||||||
|
bytes, err := sendRequest(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filesystem.NewFileFromBytes(bytes, "photo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTourGPX(detailedTour *DetailedKomootTour) (*filesystem.File, error) {
|
||||||
|
var points []*gpx.WptType
|
||||||
|
|
||||||
|
for _, item := range detailedTour.Embedded.Coordinates.Items {
|
||||||
|
t := detailedTour.Date.Unix() + int64(item.T)
|
||||||
|
|
||||||
|
points = append(points, &gpx.WptType{Lat: item.Lat, Lon: item.Lng, Ele: item.Alt, Time: time.Unix(t, 0)})
|
||||||
|
}
|
||||||
|
|
||||||
|
gpx := &gpx.GPX{
|
||||||
|
Version: "1.1",
|
||||||
|
Creator: "Strava GPX Exporter",
|
||||||
|
Trk: []*gpx.TrkType{
|
||||||
|
{
|
||||||
|
Name: detailedTour.Name,
|
||||||
|
TrkSeg: []*gpx.TrkSegType{
|
||||||
|
{
|
||||||
|
TrkPt: points,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := gpx.Write(&buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gpxFile, err := filesystem.NewFileFromBytes(buf.Bytes(), detailedTour.Name+".gpx")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gpxFile, nil
|
||||||
|
}
|
||||||
379
db/integrations/komoot/models.go
Normal file
379
db/integrations/komoot/models.go
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
package komoot
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type KomootIntegration struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
User User `json:"user"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Content struct {
|
||||||
|
HasImage bool `json:"hasImage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fitness struct {
|
||||||
|
Personalised bool `json:"personalised"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Content Content `json:"content"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
Displayname string `json:"displayname"`
|
||||||
|
Fitness Fitness `json:"fitness"`
|
||||||
|
ImageURL string `json:"imageUrl"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Metric bool `json:"metric"`
|
||||||
|
Newsletter bool `json:"newsletter"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
WelcomeMails bool `json:"welcomeMails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KomootToursResponse struct {
|
||||||
|
Embedded Embedded `json:"_embedded"`
|
||||||
|
Links ResponseLinks `json:"_links"`
|
||||||
|
Page Page `json:"page"`
|
||||||
|
}
|
||||||
|
type StartPoint struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lng float64 `json:"lng"`
|
||||||
|
Alt float64 `json:"alt"`
|
||||||
|
}
|
||||||
|
type Surfaces struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
type WayTypes struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
}
|
||||||
|
type Summary struct {
|
||||||
|
Surfaces []Surfaces `json:"surfaces"`
|
||||||
|
WayTypes []WayTypes `json:"way_types"`
|
||||||
|
}
|
||||||
|
type Difficulty struct {
|
||||||
|
Grade string `json:"grade"`
|
||||||
|
ExplanationTechnical string `json:"explanation_technical"`
|
||||||
|
ExplanationFitness string `json:"explanation_fitness"`
|
||||||
|
}
|
||||||
|
type Location struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lng float64 `json:"lng"`
|
||||||
|
}
|
||||||
|
type Path struct {
|
||||||
|
Location Location `json:"location"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
Reference string `json:"reference,omitempty"`
|
||||||
|
EndIndex int `json:"end_index,omitempty"`
|
||||||
|
SegmentType string `json:"segment_type,omitempty"`
|
||||||
|
}
|
||||||
|
type Segments struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
From int `json:"from"`
|
||||||
|
To int `json:"to"`
|
||||||
|
}
|
||||||
|
type MapImage struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attribution string `json:"attribution"`
|
||||||
|
}
|
||||||
|
type MapImagePreview struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attribution string `json:"attribution"`
|
||||||
|
}
|
||||||
|
type VectorMapImage struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attribution string `json:"attribution"`
|
||||||
|
}
|
||||||
|
type VectorMapImagePreview struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attribution string `json:"attribution"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Relation struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
}
|
||||||
|
type CreatorLinks struct {
|
||||||
|
Relation Relation `json:"relation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinksEmbedded struct {
|
||||||
|
Creator Creator `json:"creator"`
|
||||||
|
}
|
||||||
|
type LinksCreator struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksCoordinates struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksTourLine struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksParticipants struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksWayTypes struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksSurfaces struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksDirections struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksTimeline struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksTranslations struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksCoverImages struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type LinksTourRating struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type TourLinks struct {
|
||||||
|
Creator LinksCreator `json:"creator"`
|
||||||
|
Coordinates LinksCoordinates `json:"coordinates"`
|
||||||
|
TourLine LinksTourLine `json:"tour_line"`
|
||||||
|
Participants LinksParticipants `json:"participants"`
|
||||||
|
WayTypes LinksWayTypes `json:"way_types"`
|
||||||
|
Surfaces LinksSurfaces `json:"surfaces"`
|
||||||
|
Directions LinksDirections `json:"directions"`
|
||||||
|
Timeline LinksTimeline `json:"timeline"`
|
||||||
|
Translations LinksTranslations `json:"translations"`
|
||||||
|
CoverImages LinksCoverImages `json:"cover_images"`
|
||||||
|
TourRating LinksTourRating `json:"tour_rating"`
|
||||||
|
}
|
||||||
|
type KomootTour struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
RoutingVersion string `json:"routing_version"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
KcalActive int `json:"kcal_active"`
|
||||||
|
KcalResting int `json:"kcal_resting"`
|
||||||
|
StartPoint StartPoint `json:"start_point"`
|
||||||
|
Distance float64 `json:"distance"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ElevationUp float64 `json:"elevation_up"`
|
||||||
|
ElevationDown float64 `json:"elevation_down"`
|
||||||
|
Sport string `json:"sport"`
|
||||||
|
Query string `json:"query"`
|
||||||
|
Constitution int `json:"constitution"`
|
||||||
|
Summary Summary `json:"summary"`
|
||||||
|
Difficulty Difficulty `json:"difficulty"`
|
||||||
|
TourInformation []any `json:"tour_information"`
|
||||||
|
Path []Path `json:"path"`
|
||||||
|
Segments []Segments `json:"segments"`
|
||||||
|
ChangedAt time.Time `json:"changed_at"`
|
||||||
|
MapImage MapImage `json:"map_image"`
|
||||||
|
MapImagePreview MapImagePreview `json:"map_image_preview"`
|
||||||
|
VectorMapImage VectorMapImage `json:"vector_map_image"`
|
||||||
|
VectorMapImagePreview VectorMapImagePreview `json:"vector_map_image_preview"`
|
||||||
|
PotentialRouteUpdate bool `json:"potential_route_update"`
|
||||||
|
Embedded Embedded `json:"_embedded"`
|
||||||
|
Links TourLinks `json:"_links"`
|
||||||
|
}
|
||||||
|
type Embedded struct {
|
||||||
|
Tours []KomootTour `json:"tours"`
|
||||||
|
}
|
||||||
|
type Next struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
type ResponseLinks struct {
|
||||||
|
Next Next `json:"next"`
|
||||||
|
}
|
||||||
|
type Page struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
TotalElements int `json:"totalElements"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
Number int `json:"number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DetailedKomootTour struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
KcalActive float64 `json:"kcal_active"`
|
||||||
|
KcalResting float64 `json:"kcal_resting"`
|
||||||
|
StartPoint StartPoint `json:"start_point"`
|
||||||
|
Distance float64 `json:"distance"`
|
||||||
|
Duration int `json:"duration"`
|
||||||
|
ElevationUp float64 `json:"elevation_up"`
|
||||||
|
ElevationDown float64 `json:"elevation_down"`
|
||||||
|
Sport string `json:"sport"`
|
||||||
|
MapImage MapImage `json:"map_image"`
|
||||||
|
Difficulty Difficulty `json:"difficulty"`
|
||||||
|
ChangedAt time.Time `json:"changed_at"`
|
||||||
|
Embedded DetailedTourEmbedded `json:"_embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Items struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lng float64 `json:"lng"`
|
||||||
|
Alt float64 `json:"alt"`
|
||||||
|
T int `json:"t"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinates struct {
|
||||||
|
Items []Items `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DetailedTourEmbedded struct {
|
||||||
|
Coordinates Coordinates `json:"coordinates"`
|
||||||
|
Timeline Timeline `json:"timeline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Timeline struct {
|
||||||
|
Embedded TimelineEmbedded `json:"_embedded"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Page Page `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimelineEmbedded struct {
|
||||||
|
Items []Item `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Cover int `json:"cover"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Embedded TimelineItemEmbedded `json:"_embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimelineItemEmbedded struct {
|
||||||
|
Reference Reference `json:"reference"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reference struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
BaseName string `json:"base_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
ChangedAt time.Time `json:"changed_at"`
|
||||||
|
Sport string `json:"sport"`
|
||||||
|
Routable bool `json:"routable"`
|
||||||
|
StartPoint Point `json:"start_point"`
|
||||||
|
MidPoint Point `json:"mid_point"`
|
||||||
|
EndPoint Point `json:"end_point"`
|
||||||
|
Distance float64 `json:"distance"`
|
||||||
|
ElevationUp float64 `json:"elevation_up"`
|
||||||
|
ElevationDown float64 `json:"elevation_down"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
WikiPOIID string `json:"wiki_poi_id"`
|
||||||
|
PoorQuality bool `json:"poor_quality"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Flagged bool `json:"flagged"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Embedded SubEmbedded `json:"_embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Point struct {
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lng float64 `json:"lng"`
|
||||||
|
Alt float64 `json:"alt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
Self Link `json:"self"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Templated bool `json:"templated,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubEmbedded struct {
|
||||||
|
Creator Creator `json:"creator"`
|
||||||
|
Images Images `json:"images"`
|
||||||
|
Tips Tips `json:"tips"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Creator struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar Avatar `json:"avatar"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
DisplayName string `json:"display_name"`
|
||||||
|
IsPremium bool `json:"is_premium"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Avatar struct {
|
||||||
|
Src string `json:"src"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Images struct {
|
||||||
|
Embedded ImagesEmbedded `json:"_embedded"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Page Page `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagesEmbedded struct {
|
||||||
|
Items []ImageItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageItem struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Src string `json:"src"`
|
||||||
|
Rating Rating `json:"rating"`
|
||||||
|
Templated bool `json:"templated"`
|
||||||
|
HighlightID int `json:"highlight_id"`
|
||||||
|
ClientHash string `json:"client_hash,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Embedded SubEmbedded `json:"_embedded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rating struct {
|
||||||
|
Up int `json:"up"`
|
||||||
|
Down int `json:"down"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tips struct {
|
||||||
|
Embedded TipsEmbedded `json:"_embedded"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Page Page `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TipsEmbedded struct {
|
||||||
|
Items []TipItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TipItem struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Rating Rating `json:"rating"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
TextLanguage string `json:"text_language"`
|
||||||
|
TranslatedText string `json:"translated_text"`
|
||||||
|
TranslatedTextLanguage string `json:"translated_text_language"`
|
||||||
|
Attribution string `json:"attribution"`
|
||||||
|
HighlightID int `json:"highlight_id"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
Embedded SubEmbedded `json:"_embedded"`
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cron
|
package strava
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ type RefreshTokenRequest struct {
|
|||||||
type RefreshTokenResponse struct {
|
type RefreshTokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
ExpiresAt *int64 `json:"expires_at"`
|
ExpiresAt int64 `json:"expires_at"`
|
||||||
}
|
}
|
||||||
type StravaIntegration struct {
|
type StravaIntegration struct {
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
Routes bool `json:"routes"`
|
Routes bool `json:"routes"`
|
||||||
Activities bool `json:"activities"`
|
Activities bool `json:"activities"`
|
||||||
ClientID int32 `json:"clientId"`
|
ClientID int32 `json:"clientId"`
|
||||||
ClientSecret string `json:"clientSecret"`
|
ClientSecret string `json:"clientSecret"`
|
||||||
AccessToken *string `json:"accessToken,omitempty"`
|
AccessToken string `json:"accessToken,omitempty"`
|
||||||
RefreshToken *string `json:"refreshToken,omitempty"`
|
RefreshToken string `json:"refreshToken,omitempty"`
|
||||||
ExpiresAt *int64 `json:"expiresAt,omitempty"`
|
ExpiresAt int64 `json:"expiresAt,omitempty"`
|
||||||
}
|
}
|
||||||
type StravaRoute struct {
|
type StravaRoute struct {
|
||||||
Athlete Athlete `json:"athlete"`
|
Athlete Athlete `json:"athlete"`
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cron
|
package strava
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -18,6 +18,10 @@ import (
|
|||||||
"github.com/twpayne/go-polyline"
|
"github.com/twpayne/go-polyline"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type StravaApi struct {
|
||||||
|
AceessToken string
|
||||||
|
}
|
||||||
|
|
||||||
func SyncStrava(app *pocketbase.PocketBase) error {
|
func SyncStrava(app *pocketbase.PocketBase) error {
|
||||||
integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("true"))
|
integrations, err := app.Dao().FindRecordsByExpr("integrations", dbx.NewExp("true"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30,17 +34,26 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
|||||||
var stravaIntegration StravaIntegration
|
var stravaIntegration StravaIntegration
|
||||||
json.Unmarshal([]byte(stravaString), &stravaIntegration)
|
json.Unmarshal([]byte(stravaString), &stravaIntegration)
|
||||||
|
|
||||||
if !stravaIntegration.Active || stravaIntegration.RefreshToken == nil {
|
if !stravaIntegration.Active || stravaIntegration.RefreshToken == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := refreshStravaToken(stravaIntegration.ClientID, stravaIntegration.ClientSecret, *stravaIntegration.RefreshToken)
|
r, err := refreshStravaToken(stravaIntegration.ClientID, stravaIntegration.ClientSecret, stravaIntegration.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
warning := fmt.Sprintf("error refreshing strava access token: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if r.AccessToken != "" {
|
||||||
|
stravaIntegration.AccessToken = r.AccessToken
|
||||||
|
}
|
||||||
|
if r.RefreshToken != "" {
|
||||||
|
stravaIntegration.RefreshToken = r.RefreshToken
|
||||||
|
}
|
||||||
|
if r.AccessToken != "" {
|
||||||
|
stravaIntegration.ExpiresAt = r.ExpiresAt
|
||||||
}
|
}
|
||||||
stravaIntegration.AccessToken = &r.AccessToken
|
|
||||||
stravaIntegration.RefreshToken = &r.RefreshToken
|
|
||||||
stravaIntegration.ExpiresAt = r.ExpiresAt
|
|
||||||
|
|
||||||
b, err := json.Marshal(stravaIntegration)
|
b, err := json.Marshal(stravaIntegration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,11 +69,17 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
|||||||
|
|
||||||
routes, err := fetchStravaRoutes(r.AccessToken, page)
|
routes, err := fetchStravaRoutes(r.AccessToken, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
warning := fmt.Sprintf("error fetching routes from strava: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, routes)
|
hasNewRoutes, err = syncTrailsWithRoutes(app, r.AccessToken, userId, routes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
warning := fmt.Sprintf("error syncing strava routes with trails: %v\n", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
@@ -71,11 +90,17 @@ func SyncStrava(app *pocketbase.PocketBase) error {
|
|||||||
for hasNewActivities {
|
for hasNewActivities {
|
||||||
activities, err := fetchStravaActivities(r.AccessToken, page)
|
activities, err := fetchStravaActivities(r.AccessToken, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
warning := fmt.Sprintf("error fetching activities from strava: %v", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, activities)
|
hasNewActivities, err = syncTrailsWithActivities(app, r.AccessToken, userId, activities)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
warning := fmt.Sprintf("error syncing strava activities with trails: %v", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Warn(warning)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
@@ -263,7 +288,7 @@ func createTrailFromRoute(app *pocketbase.PocketBase, route StravaRoute, gpx *fi
|
|||||||
lat = coords[0][0]
|
lat = coords[0][0]
|
||||||
lon = coords[0][1]
|
lon = coords[0][1]
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Warning: No coordinates available, setting lat/lon to 0")
|
app.Logger().Warn("Warning: No coordinates available, setting lat/lon to 0")
|
||||||
lat, lon = 0, 0
|
lat, lon = 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +341,7 @@ func createWaypointsFromRoute(app *pocketbase.PocketBase, route StravaRoute, use
|
|||||||
for i, wp := range route.Waypoints {
|
for i, wp := range route.Waypoints {
|
||||||
record := models.NewRecord(collection)
|
record := models.NewRecord(collection)
|
||||||
|
|
||||||
record.Set("name", string(i))
|
record.Set("name", strconv.Itoa(i))
|
||||||
record.Set("description", wp.Description)
|
record.Set("description", wp.Description)
|
||||||
record.Set("lat", wp.Latlng[0])
|
record.Set("lat", wp.Latlng[0])
|
||||||
record.Set("lon", wp.Latlng[1])
|
record.Set("lon", wp.Latlng[1])
|
||||||
21
db/main.go
21
db/main.go
@@ -18,11 +18,12 @@ import (
|
|||||||
"github.com/pocketbase/pocketbase/forms"
|
"github.com/pocketbase/pocketbase/forms"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||||||
pbCron "github.com/pocketbase/pocketbase/tools/cron"
|
"github.com/pocketbase/pocketbase/tools/cron"
|
||||||
"github.com/pocketbase/pocketbase/tools/filesystem"
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
||||||
"github.com/pocketbase/pocketbase/tools/hook"
|
"github.com/pocketbase/pocketbase/tools/hook"
|
||||||
|
|
||||||
"pocketbase/cron"
|
"pocketbase/integrations/komoot"
|
||||||
|
"pocketbase/integrations/strava"
|
||||||
_ "pocketbase/migrations"
|
_ "pocketbase/migrations"
|
||||||
"pocketbase/util"
|
"pocketbase/util"
|
||||||
)
|
)
|
||||||
@@ -404,12 +405,20 @@ func registerRoutes(e *core.ServeEvent, app *pocketbase.PocketBase, client meili
|
|||||||
}
|
}
|
||||||
|
|
||||||
func registerCronJobs(app *pocketbase.PocketBase) {
|
func registerCronJobs(app *pocketbase.PocketBase) {
|
||||||
scheduler := pbCron.New()
|
scheduler := cron.New()
|
||||||
|
|
||||||
scheduler.MustAdd("strava", "*/15 * * * *", func() {
|
scheduler.MustAdd("integrations", "0 * * * *", func() {
|
||||||
err := cron.SyncStrava(app)
|
err := strava.SyncStrava(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.Logger().Warn(fmt.Sprintf("Error syncing with strava: %v", err))
|
warning := fmt.Sprintf("Error syncing with strava: %v", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Error(warning)
|
||||||
|
}
|
||||||
|
err = komoot.SyncKomoot(app)
|
||||||
|
if err != nil {
|
||||||
|
warning := fmt.Sprintf("Error syncing with komoot: %v", err)
|
||||||
|
fmt.Print(warning)
|
||||||
|
app.Logger().Error(warning)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
75
db/migrations/1738690714_updated_trails.go
Normal file
75
db/migrations/1738690714_updated_trails.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_external_provider := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "htr35nha",
|
||||||
|
"name": "external_provider",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"strava",
|
||||||
|
"komoot"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`), edit_external_provider); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_external_provider)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_external_provider := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "htr35nha",
|
||||||
|
"name": "external_provider",
|
||||||
|
"type": "select",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSelect": 1,
|
||||||
|
"values": [
|
||||||
|
"strava"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}`), edit_external_provider); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_external_provider)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
53
db/migrations/1738690729_updated_integrations.go
Normal file
53
db/migrations/1738690729_updated_integrations.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("iz4sezoehde64wp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add
|
||||||
|
new_komoot := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "6s0oxqgp",
|
||||||
|
"name": "komoot",
|
||||||
|
"type": "json",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"maxSize": 2000000
|
||||||
|
}
|
||||||
|
}`), new_komoot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(new_komoot)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("iz4sezoehde64wp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.Schema.RemoveField("6s0oxqgp")
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if activity.photos.length}
|
{#if activity.photos.length}
|
||||||
<div
|
<div
|
||||||
class="grid {activity.photos.length > 1
|
class="grid gap-[1px] {activity.photos.length > 1
|
||||||
? 'grid-cols-[8fr_5fr]'
|
? 'grid-cols-[8fr_5fr]'
|
||||||
: 'grid-cols-1'}"
|
: 'grid-cols-1'}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Toggle from "$lib/components/base/toggle.svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onclick: () => void;
|
||||||
|
ontoggle: (value: boolean) => void;
|
||||||
|
active: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
img: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
onclick,
|
||||||
|
ontoggle,
|
||||||
|
active = $bindable(),
|
||||||
|
disabled,
|
||||||
|
img,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
<div class="border border-input-border rounded-lg p-4 space-y-4">
|
||||||
|
<img class="h-20" src={img} alt="integration logo"/>
|
||||||
|
<div>
|
||||||
|
<h5 class="text-xl font-semibold">{title}</h5>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button class="btn-secondary" {onclick}
|
||||||
|
><i class="fa fa-cogs mr-2"></i>{$_("settings")}</button
|
||||||
|
>
|
||||||
|
<Toggle bind:value={active} onchange={ontoggle} {disabled}></Toggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import { KomootSchema } from "$lib/models/api/integration_schema";
|
||||||
|
import type {
|
||||||
|
Integration,
|
||||||
|
KomootIntegration,
|
||||||
|
} from "$lib/models/integration";
|
||||||
|
import { validator } from "@felte/validator-zod";
|
||||||
|
import { createForm } from "felte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
integration?: Integration;
|
||||||
|
onsave?: (komootIntegration: KomootIntegration) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { integration, onsave }: Props = $props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
errors.set({})
|
||||||
|
modal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
data: d,
|
||||||
|
} = createForm({
|
||||||
|
initialValues: {
|
||||||
|
email: integration?.komoot?.email ?? "",
|
||||||
|
password: integration?.komoot?.password ?? "",
|
||||||
|
active: integration?.komoot?.active ?? false,
|
||||||
|
},
|
||||||
|
extend: validator({
|
||||||
|
schema: KomootSchema,
|
||||||
|
}),
|
||||||
|
onSubmit: async (form) => {
|
||||||
|
onsave?.(form);
|
||||||
|
modal.closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="komoot-settings-modal"
|
||||||
|
size="max-w-lg"
|
||||||
|
title={"komoot " + $_("settings")}
|
||||||
|
bind:this={modal}
|
||||||
|
>
|
||||||
|
{#snippet content()}
|
||||||
|
<form id="komoot-settings-form" class="space-y-2" use:form>
|
||||||
|
<TextField
|
||||||
|
label={$_("email")}
|
||||||
|
placeholder="user@example.com"
|
||||||
|
name="email"
|
||||||
|
error={$errors.email}
|
||||||
|
></TextField>
|
||||||
|
<TextField
|
||||||
|
label={$_("password")}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
error={$errors.password}
|
||||||
|
></TextField>
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
form="komoot-settings-form"
|
||||||
|
type="submit"
|
||||||
|
name="save">{$_("save")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}</Modal
|
||||||
|
>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
|
import Toggle from "$lib/components/base/toggle.svelte";
|
||||||
|
import { StravaSchema } from "$lib/models/api/integration_schema";
|
||||||
|
import type {
|
||||||
|
Integration,
|
||||||
|
StravaIntegration,
|
||||||
|
} from "$lib/models/integration";
|
||||||
|
import { validator } from "@felte/validator-zod";
|
||||||
|
import { createForm } from "felte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
integration?: Integration;
|
||||||
|
onsave?: (stravaIntegration: StravaIntegration) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { integration, onsave }: Props = $props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
errors.set({})
|
||||||
|
modal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
} = createForm({
|
||||||
|
initialValues: {
|
||||||
|
clientId: integration?.strava?.clientId ?? "",
|
||||||
|
clientSecret: integration?.strava?.clientSecret ?? "",
|
||||||
|
routes: integration?.strava?.routes ?? true,
|
||||||
|
activities: integration?.strava?.activities ?? true,
|
||||||
|
active: integration?.strava?.active ?? false,
|
||||||
|
},
|
||||||
|
extend: validator({
|
||||||
|
schema: StravaSchema,
|
||||||
|
}),
|
||||||
|
onSubmit: async (form) => {
|
||||||
|
onsave?.(form);
|
||||||
|
modal.closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="strava-settings-modal"
|
||||||
|
size="max-w-lg"
|
||||||
|
title={"strava " + $_("settings")}
|
||||||
|
bind:this={modal}
|
||||||
|
>
|
||||||
|
{#snippet content()}
|
||||||
|
<form id="strava-settings-form" class="space-y-2" use:form>
|
||||||
|
<TextField
|
||||||
|
label="Client-ID"
|
||||||
|
placeholder="000000"
|
||||||
|
name="clientId"
|
||||||
|
error={$errors.clientId}
|
||||||
|
></TextField>
|
||||||
|
<TextField
|
||||||
|
label="Client Secret"
|
||||||
|
placeholder="de8b3789bd7116d..."
|
||||||
|
name="clientSecret"
|
||||||
|
type="password"
|
||||||
|
error={$errors.clientSecret}
|
||||||
|
></TextField>
|
||||||
|
<div class="flex gap-x-4">
|
||||||
|
<Toggle name="routes" label={$_("route", { values: { n: 2 } })}
|
||||||
|
></Toggle>
|
||||||
|
<Toggle
|
||||||
|
name="activities"
|
||||||
|
label={$_("activity", { values: { n: 2 } })}
|
||||||
|
></Toggle>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
form="strava-settings-form"
|
||||||
|
type="submit"
|
||||||
|
name="save">{$_("save")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}</Modal
|
||||||
|
>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<div
|
<div
|
||||||
class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"
|
class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"
|
||||||
>
|
>
|
||||||
<img width="100%" id="header-img" src={thumbnail} alt="" />
|
<img class="min-h-40" id="header-img" src={thumbnail} alt="" />
|
||||||
</div>
|
</div>
|
||||||
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
{#if (trail.public || trailIsShared) && pb.authStore.model}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -44,13 +44,20 @@
|
|||||||
import TrailTimeline from "./trail_timeline.svelte";
|
import TrailTimeline from "./trail_timeline.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trail: Trail;
|
initTrail: Trail;
|
||||||
mode?: "overview" | "map" | "list";
|
mode?: "overview" | "map" | "list";
|
||||||
markers?: M.Marker[];
|
markers?: M.Marker[];
|
||||||
activeTab?: number;
|
activeTab?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { trail, mode = "map", markers = [], activeTab = 0 }: Props = $props();
|
let {
|
||||||
|
initTrail,
|
||||||
|
mode = "map",
|
||||||
|
markers = [],
|
||||||
|
activeTab = 0,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let trail = $state(initTrail);
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
$_("summit-book"),
|
$_("summit-book"),
|
||||||
@@ -362,7 +369,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<EmptyStateDescription></EmptyStateDescription>
|
<EmptyStateDescription></EmptyStateDescription>
|
||||||
{/if}
|
{/if}
|
||||||
<h4 class="text-2xl font-semibold mb-6 mt-12">{$_("route", { values: { n: 2 } })}</h4>
|
<h4 class="text-2xl font-semibold mb-6 mt-12">
|
||||||
|
{$_("route", { values: { n: 2 } })}
|
||||||
|
</h4>
|
||||||
{#if mode === "overview"}
|
{#if mode === "overview"}
|
||||||
<div
|
<div
|
||||||
class="relative border border-input-border rounded-xl p-2 mb-6 text-xs"
|
class="relative border border-input-border rounded-xl p-2 mb-6 text-xs"
|
||||||
|
|||||||
@@ -42,23 +42,26 @@
|
|||||||
|
|
||||||
<div class="border border-input-border rounded-xl overflow-hidden">
|
<div class="border border-input-border rounded-xl overflow-hidden">
|
||||||
{#if wp.photos.length}
|
{#if wp.photos.length}
|
||||||
<PhotoGallery photos={wp.photos.map((p) => getFileURL(wp, p))} bind:this={gallery[i]}
|
<PhotoGallery
|
||||||
|
photos={wp.photos.map((p) => getFileURL(wp, p))}
|
||||||
|
bind:this={gallery[i]}
|
||||||
></PhotoGallery>
|
></PhotoGallery>
|
||||||
<div
|
<div
|
||||||
class="grid {wp.photos.length > 1
|
class="grid gap-[1px] {wp.photos.length > 1
|
||||||
? 'grid-cols-[8fr_5fr]'
|
? 'grid-cols-[8fr_5fr]'
|
||||||
: 'grid-cols-1'} cursor-pointer"
|
: 'grid-cols-1'} cursor-pointer"
|
||||||
>
|
>
|
||||||
{#each wp.photos as photo, j}
|
{#each wp.photos as photo, j}
|
||||||
<button onclick={() => gallery[i].openGallery(j)}>
|
<img
|
||||||
<img
|
onclick={() => gallery[i].openGallery(j)}
|
||||||
class="object-cover h-full max-h-80 w-full"
|
role="presentation"
|
||||||
class:row-span-2={i == 0 &&
|
class="w-full object-cover {j == 0 &&
|
||||||
wp.photos.length > 2}
|
wp.photos.length > 2
|
||||||
src={getFileURL(wp, photo)}
|
? 'row-span-2 h-80'
|
||||||
alt=""
|
: 'h-[159.5px]'}"
|
||||||
/>
|
src={getFileURL(wp, photo)}
|
||||||
</button>
|
alt=""
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 p-4 outline outline-1 outline-input-border rounded-md my-2 hover:outline-2"
|
class="flex gap-4 p-4 outline outline-1 outline-input-border rounded-md my-2 hover:outline-2 items-start"
|
||||||
>
|
>
|
||||||
{#if imgSrc.length}
|
{#if imgSrc.length}
|
||||||
{#if mode == "show"}
|
{#if mode == "show"}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
>
|
>
|
||||||
{#each imgSrc as img, i}
|
{#each imgSrc as img, i}
|
||||||
<img
|
<img
|
||||||
class="absolute h-full rounded-xl object-cover"
|
class="absolute h-full rounded-xl object-cover aspect-square"
|
||||||
style="top: {6 * i}px; right: {6 *
|
style="top: {6 * i}px; right: {6 *
|
||||||
i}px; transform: rotate(-{i * 5}deg)"
|
i}px; transform: rotate(-{i * 5}deg)"
|
||||||
src={img}
|
src={img}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Fehler beim Login",
|
"error-during-login": "Fehler beim Login",
|
||||||
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
|
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
|
||||||
"error-exporting-trail": "Fehler beim Exportieren des Trails",
|
"error-exporting-trail": "Fehler beim Exportieren des Trails",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Fehler beim Posten des Kommentars",
|
"error-posting-comment": "Fehler beim Posten des Kommentars",
|
||||||
"error-printing-map": "Fehler beim Drucken der Karte",
|
"error-printing-map": "Fehler beim Drucken der Karte",
|
||||||
"error-reading-file": "Fehler beim Lesen der Datei",
|
"error-reading-file": "Fehler beim Lesen der Datei",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Fehler beim Speichern der Route",
|
"error-saving-trail": "Fehler beim Speichern der Route",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Fehler beim Aktualisieren des Passworts",
|
"error-updating-password": "Fehler beim Aktualisieren des Passworts",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Gesch. Dauer",
|
"est-duration": "Gesch. Dauer",
|
||||||
"explore": "Erkunden",
|
"explore": "Erkunden",
|
||||||
"explore-some-trails": "Erkunde einige Routen",
|
"explore-some-trails": "Erkunde einige Routen",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importieren",
|
"import": "Importieren",
|
||||||
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
|
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
|
||||||
"include-description": "Beschreibung übernehmen",
|
"include-description": "Beschreibung übernehmen",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Ungültiges Datum",
|
"invalid-date": "Ungültiges Datum",
|
||||||
"invalid-username": "Ungültiger Nutzername",
|
"invalid-username": "Ungültiger Nutzername",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Deine Listen sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Listen ändern.",
|
"settings-privacy-lists-public": "Deine Listen sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Listen ändern.",
|
||||||
"settings-privacy-trails-private": "Deine Routen sind standardmäßig privat. \nNiemand außer Ihnen kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
|
"settings-privacy-trails-private": "Deine Routen sind standardmäßig privat. \nNiemand außer Ihnen kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
|
||||||
"settings-privacy-trails-public": "Deine Trails sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
|
"settings-privacy-trails-public": "Deine Trails sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Teilen",
|
"share": "Teilen",
|
||||||
"share-profile": "Profil teilen",
|
"share-profile": "Profil teilen",
|
||||||
"share-this-list": "Diese Liste teilen",
|
"share-this-list": "Diese Liste teilen",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Error during login",
|
"error-during-login": "Error during login",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
|
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Error printing map",
|
"error-printing-map": "Error printing map",
|
||||||
"error-reading-file": "Error reading file",
|
"error-reading-file": "Error reading file",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Error saving trail",
|
"error-saving-trail": "Error saving trail",
|
||||||
"error-setting-up-strava-integration": "Error setting up strava integration",
|
"error-setting-up-strava-integration": "Error setting up strava integration",
|
||||||
"error-updating-password": "Error updating password",
|
"error-updating-password": "Error updating password",
|
||||||
|
"error-updating-strava-integration": "Error updating komoot integration",
|
||||||
"est-duration": "Est. duration",
|
"est-duration": "Est. duration",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"explore-some-trails": "Explore some trails",
|
"explore-some-trails": "Explore some trails",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Import",
|
"import": "Import",
|
||||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||||
"include-description": "Include description",
|
"include-description": "Include description",
|
||||||
|
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
|
||||||
|
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
|
||||||
"integrations": "Integrations",
|
"integrations": "Integrations",
|
||||||
"invalid-date": "Invalid Date",
|
"invalid-date": "Invalid Date",
|
||||||
"invalid-username": "Invalid username",
|
"invalid-username": "Invalid username",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "Settings saved",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "Share this list",
|
"share-this-list": "Share this list",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Error durante el acceso",
|
"error-during-login": "Error durante el acceso",
|
||||||
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
|
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
|
||||||
"error-exporting-trail": "Error exportando la ruta",
|
"error-exporting-trail": "Error exportando la ruta",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error publicando el comentario",
|
"error-posting-comment": "Error publicando el comentario",
|
||||||
"error-printing-map": "Error durante la impresión del mapa",
|
"error-printing-map": "Error durante la impresión del mapa",
|
||||||
"error-reading-file": "Error leyendo el archivo",
|
"error-reading-file": "Error leyendo el archivo",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Error guardando la ruta",
|
"error-saving-trail": "Error guardando la ruta",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Error actualizando la contraseña",
|
"error-updating-password": "Error actualizando la contraseña",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Duración estimada",
|
"est-duration": "Duración estimada",
|
||||||
"explore": "Explora",
|
"explore": "Explora",
|
||||||
"explore-some-trails": "Explora alguna ruta",
|
"explore-some-trails": "Explora alguna ruta",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
|
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
|
||||||
"include-description": "Incluir descripción",
|
"include-description": "Incluir descripción",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Fecha no válida",
|
"invalid-date": "Fecha no válida",
|
||||||
"invalid-username": "Usuario no válido",
|
"invalid-username": "Usuario no válido",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Tus listas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para listas específicas.",
|
"settings-privacy-lists-public": "Tus listas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para listas específicas.",
|
||||||
"settings-privacy-trails-private": "Tus rutas son privadas por defecto. Nadie excepto tú podrás verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
"settings-privacy-trails-private": "Tus rutas son privadas por defecto. Nadie excepto tú podrás verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
||||||
"settings-privacy-trails-public": "Tus rutas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
"settings-privacy-trails-public": "Tus rutas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Compartir",
|
"share": "Compartir",
|
||||||
"share-profile": "Compartir perfil",
|
"share-profile": "Compartir perfil",
|
||||||
"share-this-list": "Compartir esta lista",
|
"share-this-list": "Compartir esta lista",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Erreur durant la connexion",
|
"error-during-login": "Erreur durant la connexion",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
|
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Erreur d'impression de la carte",
|
"error-printing-map": "Erreur d'impression de la carte",
|
||||||
"error-reading-file": "Erreur de lecture du fichier",
|
"error-reading-file": "Erreur de lecture du fichier",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Erreur lors de l'enregistrement de l'itinéraire",
|
"error-saving-trail": "Erreur lors de l'enregistrement de l'itinéraire",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Erreur lors de la mise à jour du mot de passe",
|
"error-updating-password": "Erreur lors de la mise à jour du mot de passe",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Temps estimé",
|
"est-duration": "Temps estimé",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"explore-some-trails": "Explorer les itinéraires",
|
"explore-some-trails": "Explorer les itinéraires",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importer",
|
"import": "Importer",
|
||||||
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
|
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
|
||||||
"include-description": "Inclure la description",
|
"include-description": "Inclure la description",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Date invalide",
|
"invalid-date": "Date invalide",
|
||||||
"invalid-username": "Nom d'utilisateur invalide",
|
"invalid-username": "Nom d'utilisateur invalide",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Partager",
|
"share": "Partager",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "Partager cette liste",
|
"share-this-list": "Partager cette liste",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Hiba bejelentkezés közben",
|
"error-during-login": "Hiba bejelentkezés közben",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Error printing map",
|
"error-printing-map": "Error printing map",
|
||||||
"error-reading-file": "Hiba a fájl olvasása közben",
|
"error-reading-file": "Hiba a fájl olvasása közben",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Error saving trail",
|
"error-saving-trail": "Error saving trail",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Error updating password",
|
"error-updating-password": "Error updating password",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Becsült időtartam",
|
"est-duration": "Becsült időtartam",
|
||||||
"explore": "Felfedezés",
|
"explore": "Felfedezés",
|
||||||
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Import",
|
"import": "Import",
|
||||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||||
"include-description": "Include description",
|
"include-description": "Include description",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Érvénytelen dátum",
|
"invalid-date": "Érvénytelen dátum",
|
||||||
"invalid-username": "Érvénytelen felhasználó",
|
"invalid-username": "Érvénytelen felhasználó",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "Share this list",
|
"share-this-list": "Share this list",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Errore durante il login",
|
"error-during-login": "Errore durante il login",
|
||||||
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
|
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
|
||||||
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Errore pubblicando il commento",
|
"error-posting-comment": "Errore pubblicando il commento",
|
||||||
"error-printing-map": "Errore durante la stampa della mappa",
|
"error-printing-map": "Errore durante la stampa della mappa",
|
||||||
"error-reading-file": "Errore durante la lettura del file",
|
"error-reading-file": "Errore durante la lettura del file",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Errore nel salvataggio del percorso",
|
"error-saving-trail": "Errore nel salvataggio del percorso",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Errore nell'aggiornamento della password",
|
"error-updating-password": "Errore nell'aggiornamento della password",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Durata stimata",
|
"est-duration": "Durata stimata",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
"explore-some-trails": "Esplora alcuni percorsi",
|
"explore-some-trails": "Esplora alcuni percorsi",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importa",
|
"import": "Importa",
|
||||||
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
|
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
|
||||||
"include-description": "Adotta descrizione",
|
"include-description": "Adotta descrizione",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Data non valida",
|
"invalid-date": "Data non valida",
|
||||||
"invalid-username": "Nome utente non valido",
|
"invalid-username": "Nome utente non valido",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Le tue liste sono pubbliche per impostazione predefinita. Tutto potrànno vederle. Puoi cambiare queste impostazione in qualsiasi momento per liste specifiche.",
|
"settings-privacy-lists-public": "Le tue liste sono pubbliche per impostazione predefinita. Tutto potrànno vederle. Puoi cambiare queste impostazione in qualsiasi momento per liste specifiche.",
|
||||||
"settings-privacy-trails-private": "Le tuoi percorsi sono privati per impostazione predefinita. Nessuno tranne tu potrà vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
|
"settings-privacy-trails-private": "Le tuoi percorsi sono privati per impostazione predefinita. Nessuno tranne tu potrà vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
|
||||||
"settings-privacy-trails-public": "Le tuoi percorsi sono pubblici per impostazione predefinita. Tutti potranno vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
|
"settings-privacy-trails-public": "Le tuoi percorsi sono pubblici per impostazione predefinita. Tutti potranno vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Condividi",
|
"share": "Condividi",
|
||||||
"share-profile": "Condividere profilo",
|
"share-profile": "Condividere profilo",
|
||||||
"share-this-list": "Condividi questa lista",
|
"share-this-list": "Condividi questa lista",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Het inloggen is mislukt",
|
"error-during-login": "Het inloggen is mislukt",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Fout bij afdrukken van kaart",
|
"error-printing-map": "Fout bij afdrukken van kaart",
|
||||||
"error-reading-file": "Het bestand kan niet worden ingelezen",
|
"error-reading-file": "Het bestand kan niet worden ingelezen",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Error saving trail",
|
"error-saving-trail": "Error saving trail",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Error updating password",
|
"error-updating-password": "Error updating password",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Geschatte duur",
|
"est-duration": "Geschatte duur",
|
||||||
"explore": "Verkennen",
|
"explore": "Verkennen",
|
||||||
"explore-some-trails": "Verken enkele wandelroutes",
|
"explore-some-trails": "Verken enkele wandelroutes",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Import",
|
"import": "Import",
|
||||||
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
|
||||||
"include-description": "Inclusief beschrijving",
|
"include-description": "Inclusief beschrijving",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Ongeldige datum",
|
"invalid-date": "Ongeldige datum",
|
||||||
"invalid-username": "Ongeldige gebruikersnaam",
|
"invalid-username": "Ongeldige gebruikersnaam",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "Share this list",
|
"share-this-list": "Share this list",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Błąd podczas logowania",
|
"error-during-login": "Błąd podczas logowania",
|
||||||
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
|
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
|
||||||
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
|
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Błąd podczas drukowania mapy",
|
"error-printing-map": "Błąd podczas drukowania mapy",
|
||||||
"error-reading-file": "Błąd wczytywania pliku",
|
"error-reading-file": "Błąd wczytywania pliku",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Błąd podczas zapisywania szlaku",
|
"error-saving-trail": "Błąd podczas zapisywania szlaku",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Błąd podczas aktualizacji hasła",
|
"error-updating-password": "Błąd podczas aktualizacji hasła",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Szacowany czas",
|
"est-duration": "Szacowany czas",
|
||||||
"explore": "Eksploruj",
|
"explore": "Eksploruj",
|
||||||
"explore-some-trails": "Eksploruj różne szlaki",
|
"explore-some-trails": "Eksploruj różne szlaki",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importuj",
|
"import": "Importuj",
|
||||||
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
|
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
|
||||||
"include-description": "Dołącz opis",
|
"include-description": "Dołącz opis",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Nieprawidłowa data",
|
"invalid-date": "Nieprawidłowa data",
|
||||||
"invalid-username": "Błędna nazwa użytkownika",
|
"invalid-username": "Błędna nazwa użytkownika",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Domyślnie, twoje listy są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdej listy.",
|
"settings-privacy-lists-public": "Domyślnie, twoje listy są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdej listy.",
|
||||||
"settings-privacy-trails-private": "Domyślnie, twoje szlaki są prywatne. Nikt poza tobą nie będzie mógł ich zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
|
"settings-privacy-trails-private": "Domyślnie, twoje szlaki są prywatne. Nikt poza tobą nie będzie mógł ich zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
|
||||||
"settings-privacy-trails-public": "Domyślnie, twoje szlaki są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
|
"settings-privacy-trails-public": "Domyślnie, twoje szlaki są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Udostępnij",
|
"share": "Udostępnij",
|
||||||
"share-profile": "Udostępnij profil",
|
"share-profile": "Udostępnij profil",
|
||||||
"share-this-list": "Udostępnij tę listę",
|
"share-this-list": "Udostępnij tę listę",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "Erro durante o ‘login’",
|
"error-during-login": "Erro durante o ‘login’",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Erro na exportação do percurso",
|
"error-exporting-trail": "Erro na exportação do percurso",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "Erro na impressão do mapa",
|
"error-printing-map": "Erro na impressão do mapa",
|
||||||
"error-reading-file": "Erro ao ler o arquivo",
|
"error-reading-file": "Erro ao ler o arquivo",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "Erro ao gravar percurso",
|
"error-saving-trail": "Erro ao gravar percurso",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "Erro ao atualizar password",
|
"error-updating-password": "Erro ao atualizar password",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "Duração prevista",
|
"est-duration": "Duração prevista",
|
||||||
"explore": "Explorar",
|
"explore": "Explorar",
|
||||||
"explore-some-trails": "Explore algumas trilhas",
|
"explore-some-trails": "Explore algumas trilhas",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
|
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
|
||||||
"include-description": "Incluir descrição",
|
"include-description": "Incluir descrição",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "Data inválida",
|
"invalid-date": "Data inválida",
|
||||||
"invalid-username": "Nome de usuário inválido",
|
"invalid-username": "Nome de usuário inválido",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "Partilhar",
|
"share": "Partilhar",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "Partilhar esta lista",
|
"share-this-list": "Partilhar esta lista",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"error-during-login": "登录错误",
|
"error-during-login": "登录错误",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "导出路线失败",
|
"error-exporting-trail": "导出路线失败",
|
||||||
|
"error-logging-in-to-komoot": "",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
"error-printing-map": "打印地图失败",
|
"error-printing-map": "打印地图失败",
|
||||||
"error-reading-file": "读取文件错误",
|
"error-reading-file": "读取文件错误",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"error-saving-trail": "保存路线失败",
|
"error-saving-trail": "保存路线失败",
|
||||||
"error-setting-up-strava-integration": "",
|
"error-setting-up-strava-integration": "",
|
||||||
"error-updating-password": "更新密码失败",
|
"error-updating-password": "更新密码失败",
|
||||||
|
"error-updating-strava-integration": "",
|
||||||
"est-duration": "预计时长",
|
"est-duration": "预计时长",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"explore-some-trails": "探索行程",
|
"explore-some-trails": "探索行程",
|
||||||
@@ -143,6 +145,8 @@
|
|||||||
"import": "导入",
|
"import": "导入",
|
||||||
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
|
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
|
||||||
"include-description": "包含描述",
|
"include-description": "包含描述",
|
||||||
|
"integration-description-komoot": "",
|
||||||
|
"integration-description-strava": "",
|
||||||
"integrations": "",
|
"integrations": "",
|
||||||
"invalid-date": "无效日期",
|
"invalid-date": "无效日期",
|
||||||
"invalid-username": "无效用户名",
|
"invalid-username": "无效用户名",
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
|
||||||
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
|
||||||
|
"settings-saved": "",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"share-profile": "Share profile",
|
"share-profile": "Share profile",
|
||||||
"share-this-list": "分享此列表",
|
"share-this-list": "分享此列表",
|
||||||
|
|||||||
@@ -12,14 +12,22 @@ const StravaSchema = z.object({
|
|||||||
active: z.boolean()
|
active: z.boolean()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const KomootSchema = z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
password: z.string(),
|
||||||
|
active: z.boolean()
|
||||||
|
})
|
||||||
|
|
||||||
const IntegrationCreateSchema = z.object({
|
const IntegrationCreateSchema = z.object({
|
||||||
user: z.string().length(15),
|
user: z.string().length(15),
|
||||||
strava: StravaSchema,
|
strava: StravaSchema.optional(),
|
||||||
|
komoot: KomootSchema.optional()
|
||||||
|
|
||||||
}) satisfies ZodType<Integration>
|
}) satisfies ZodType<Integration>
|
||||||
|
|
||||||
const IntegrationUpdateSchema = z.object({
|
const IntegrationUpdateSchema = z.object({
|
||||||
strava: StravaSchema.optional(),
|
strava: StravaSchema.optional().nullable(),
|
||||||
|
komoot: KomootSchema.optional().nullable()
|
||||||
}) satisfies ZodType<Partial<Integration>>
|
}) satisfies ZodType<Partial<Integration>>
|
||||||
|
|
||||||
export { StravaSchema, IntegrationCreateSchema, IntegrationUpdateSchema }
|
export { StravaSchema, IntegrationCreateSchema, IntegrationUpdateSchema, KomootSchema };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
interface BaseIntegration {
|
export interface BaseIntegration {
|
||||||
active: boolean
|
active: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StravaIntegration extends BaseIntegration {
|
export interface StravaIntegration extends BaseIntegration {
|
||||||
clientId: string | number;
|
clientId: string | number;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
routes: boolean;
|
routes: boolean;
|
||||||
@@ -13,13 +13,21 @@ interface StravaIntegration extends BaseIntegration {
|
|||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KomootIntegration extends BaseIntegration {
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Integration {
|
export class Integration {
|
||||||
id?: string;
|
id?: string;
|
||||||
user: string;
|
user: string;
|
||||||
strava?: StravaIntegration;
|
strava?: StravaIntegration | null;
|
||||||
|
komoot?: KomootIntegration | null
|
||||||
|
|
||||||
constructor(user: string, strava?: StravaIntegration) {
|
constructor(user: string, strava?: StravaIntegration, komoot?: KomootIntegration) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.strava = strava;
|
this.strava = strava;
|
||||||
|
this.komoot = komoot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
onclick={(data) => selectTrail(data.trail)}
|
onclick={(data) => selectTrail(data.trail)}
|
||||||
></ListPanel>
|
></ListPanel>
|
||||||
{:else if selectedList && selectedTrail}
|
{:else if selectedList && selectedTrail}
|
||||||
<TrailInfoPanel trail={selectedTrail} mode="list" {markers}
|
<TrailInfoPanel initTrail={selectedTrail} mode="list" {markers}
|
||||||
></TrailInfoPanel>
|
></TrailInfoPanel>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<div id="panel" class="hidden md:block">
|
<div id="panel" class="hidden md:block">
|
||||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
<TrailInfoPanel initTrail={$trail} {markers}></TrailInfoPanel>
|
||||||
</div>
|
</div>
|
||||||
<div id="trail-details">
|
<div id="trail-details">
|
||||||
<MapWithElevationMaplibre
|
<MapWithElevationMaplibre
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import Button from "$lib/components/base/button.svelte";
|
import IntegrationCard from "$lib/components/settings/integrations/integration_card.svelte";
|
||||||
import Modal from "$lib/components/base/modal.svelte";
|
import KomootSettingsModal from "$lib/components/settings/integrations/komoot_settings_modal.svelte";
|
||||||
import TextField from "$lib/components/base/text_field.svelte";
|
import StravaSettingsModal from "$lib/components/settings/integrations/strava_settings_modal.svelte";
|
||||||
import Toggle from "$lib/components/base/toggle.svelte";
|
import {
|
||||||
import { StravaSchema } from "$lib/models/api/integration_schema.js";
|
Integration,
|
||||||
import { Integration } from "$lib/models/integration.js";
|
type BaseIntegration,
|
||||||
|
type KomootIntegration,
|
||||||
|
type StravaIntegration,
|
||||||
|
} from "$lib/models/integration.js";
|
||||||
import {
|
import {
|
||||||
integrations_create,
|
integrations_create,
|
||||||
integrations_update,
|
integrations_update,
|
||||||
} from "$lib/stores/integration_store.js";
|
} from "$lib/stores/integration_store.js";
|
||||||
import { show_toast } from "$lib/stores/toast_store.js";
|
import { show_toast } from "$lib/stores/toast_store.js";
|
||||||
import { validator } from "@felte/validator-zod";
|
|
||||||
import { createForm } from "felte";
|
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
@@ -22,54 +23,44 @@
|
|||||||
const scope = "read_all,activity:read_all";
|
const scope = "read_all,activity:read_all";
|
||||||
const redirectUri = page.url.href + "/callback/strava";
|
const redirectUri = page.url.href + "/callback/strava";
|
||||||
|
|
||||||
let stravaSettingsModal: Modal;
|
let stravaSettingsModal: StravaSettingsModal;
|
||||||
let stravaToggleValue: boolean = $derived(
|
let stravaToggleValue: boolean = $derived(
|
||||||
integration?.strava?.active || false,
|
integration?.strava?.active || false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { form, errors, data: d } = createForm({
|
let komootSettingsModal: KomootSettingsModal;
|
||||||
initialValues: {
|
let komootToggleValue: boolean = $state(data.integration?.komoot?.active ?? false);
|
||||||
clientId: data.integration?.strava?.clientId ?? "",
|
|
||||||
clientSecret: data.integration?.strava?.clientSecret ?? "",
|
|
||||||
routes: data.integration?.strava?.routes ?? true,
|
|
||||||
activities: data.integration?.strava?.activities ?? true,
|
|
||||||
active: data.integration?.strava?.active ?? false,
|
|
||||||
},
|
|
||||||
extend: validator({
|
|
||||||
schema: StravaSchema,
|
|
||||||
}),
|
|
||||||
onSubmit: async (form) => {
|
|
||||||
stravaSettingsModal.closeModal();
|
|
||||||
|
|
||||||
try {
|
async function onSettingsSave(
|
||||||
if (integration) {
|
form: StravaIntegration | KomootIntegration,
|
||||||
integration.strava = {
|
key: "strava" | "komoot",
|
||||||
clientId: form.clientId,
|
) {
|
||||||
clientSecret: form.clientSecret,
|
try {
|
||||||
routes: form.routes,
|
if (integration) {
|
||||||
activities: form.activities,
|
integration[key] = form as any;
|
||||||
active: integration.strava?.active ?? false,
|
integration = await integrations_update(integration);
|
||||||
};
|
} else {
|
||||||
integration = await integrations_update(integration);
|
const newIntegration: Integration = {
|
||||||
} else {
|
user: "",
|
||||||
const newIntegration = new Integration("", {
|
[key]: form,
|
||||||
routes: form.routes,
|
};
|
||||||
activities: form.activities,
|
integration = await integrations_create(newIntegration);
|
||||||
clientId: form.clientId,
|
|
||||||
clientSecret: form.clientSecret,
|
|
||||||
active: false,
|
|
||||||
});
|
|
||||||
integration = await integrations_create(newIntegration);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
show_toast({
|
|
||||||
text: $_("error-setting-up-strava-integration"),
|
|
||||||
icon: "close",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
show_toast({
|
||||||
|
text: $_("settings-saved"),
|
||||||
|
icon: "check",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-setting-up-strava-integration"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onStravaToggle(value: boolean) {
|
async function onStravaToggle(value: boolean) {
|
||||||
if (!integration?.strava) {
|
if (!integration?.strava) {
|
||||||
return;
|
return;
|
||||||
@@ -80,21 +71,13 @@
|
|||||||
} else {
|
} else {
|
||||||
const deauthUrl = `https://www.strava.com/oauth/deauthorize`;
|
const deauthUrl = `https://www.strava.com/oauth/deauthorize`;
|
||||||
|
|
||||||
const r = await fetch(deauthUrl, {
|
await fetch(deauthUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${integration.strava.accessToken}`,
|
Authorization: `Bearer ${integration.strava.accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r.ok) {
|
|
||||||
show_toast({
|
|
||||||
text: $_("error-disabling-strava-integration"),
|
|
||||||
icon: "close",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
integration.strava = {
|
integration.strava = {
|
||||||
clientId: integration.strava.clientId,
|
clientId: integration.strava.clientId,
|
||||||
clientSecret: integration.strava.clientSecret,
|
clientSecret: integration.strava.clientSecret,
|
||||||
@@ -116,6 +99,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onKomootToggle(value: boolean) {
|
||||||
|
if (!integration?.komoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
const authUrl = `https://api.komoot.de/v006/account/email/${integration.komoot.email}/`;
|
||||||
|
const r = await fetch(authUrl, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${btoa(integration.komoot.email + ":" + integration.komoot.password)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
komootToggleValue = false;
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-logging-in-to-komoot"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
integration.komoot.active = true;
|
||||||
|
} else {
|
||||||
|
integration.komoot.active = false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
integration = await integrations_update(integration);
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-updating-strava-integration"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -125,77 +143,35 @@
|
|||||||
<h3 class="text-2xl font-semibold">{$_("integrations")}</h3>
|
<h3 class="text-2xl font-semibold">{$_("integrations")}</h3>
|
||||||
<hr class="mt-4 mb-6 border-input-border" />
|
<hr class="mt-4 mb-6 border-input-border" />
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div class="border border-input-border rounded-lg p-4 space-y-4">
|
<IntegrationCard
|
||||||
<img
|
img="https://upload.wikimedia.org/wikipedia/commons/c/cb/Strava_Logo.svg"
|
||||||
src="https://upload.wikimedia.org/wikipedia/commons/c/cb/Strava_Logo.svg"
|
title="strava"
|
||||||
alt="strava logo"
|
description={$_("integration-description-strava")}
|
||||||
/>
|
disabled={!integration?.strava}
|
||||||
<div>
|
active={stravaToggleValue}
|
||||||
<h5 class="text-xl font-semibold">strava</h5>
|
onclick={() => stravaSettingsModal.openModal()}
|
||||||
<p class="text-sm text-gray-500">
|
ontoggle={onStravaToggle}
|
||||||
Syncs your strava routes with wanderer in regular intervals.
|
></IntegrationCard>
|
||||||
</p>
|
<IntegrationCard
|
||||||
</div>
|
img="https://upload.wikimedia.org/wikipedia/commons/8/82/Komoot-logo-type.svg"
|
||||||
<div class="flex items-center justify-between">
|
title="komoot"
|
||||||
<button
|
description={$_("integration-description-komoot")}
|
||||||
class="btn-secondary"
|
disabled={!integration?.komoot}
|
||||||
onclick={() => stravaSettingsModal.openModal()}
|
bind:active={komootToggleValue}
|
||||||
><i class="fa fa-cogs mr-2"></i>{$_("settings")}</button
|
onclick={() => komootSettingsModal.openModal()}
|
||||||
>
|
ontoggle={onKomootToggle}
|
||||||
<Toggle
|
></IntegrationCard>
|
||||||
value={stravaToggleValue}
|
|
||||||
onchange={onStravaToggle}
|
|
||||||
disabled={!integration?.strava}
|
|
||||||
></Toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<StravaSettingsModal
|
||||||
id="strava-settings-modal"
|
|
||||||
size="max-w-lg"
|
|
||||||
title={"strava " + $_("settings")}
|
|
||||||
bind:this={stravaSettingsModal}
|
bind:this={stravaSettingsModal}
|
||||||
>
|
{integration}
|
||||||
{#snippet content()}
|
onsave={(form) => onSettingsSave(form, "strava")}
|
||||||
<form id="strava-settings-form" class="space-y-2" use:form>
|
></StravaSettingsModal>
|
||||||
<TextField
|
|
||||||
label="Client-ID"
|
<KomootSettingsModal
|
||||||
placeholder="000000"
|
bind:this={komootSettingsModal}
|
||||||
name="clientId"
|
{integration}
|
||||||
error={$errors.clientId}
|
onsave={(form) => onSettingsSave(form, "komoot")}
|
||||||
></TextField>
|
></KomootSettingsModal>
|
||||||
<TextField
|
|
||||||
label="Client Secret"
|
|
||||||
placeholder="de8b3789bd7116d..."
|
|
||||||
name="clientSecret"
|
|
||||||
type="password"
|
|
||||||
error={$errors.clientSecret}
|
|
||||||
></TextField>
|
|
||||||
<div class="flex gap-x-4">
|
|
||||||
<Toggle name="routes" label={$_("route", { values: { n: 2 } })}
|
|
||||||
></Toggle>
|
|
||||||
<Toggle
|
|
||||||
name="activities"
|
|
||||||
label={$_("activity", { values: { n: 2 } })}
|
|
||||||
></Toggle>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet footer()}
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<button
|
|
||||||
class="btn-secondary"
|
|
||||||
onclick={() => stravaSettingsModal.closeModal()}
|
|
||||||
>{$_("cancel")}</button
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn-primary"
|
|
||||||
form="strava-settings-form"
|
|
||||||
type="submit"
|
|
||||||
name="save">{$_("save")}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/snippet}</Modal
|
|
||||||
>
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { error, redirect, type RequestEvent, type ServerLoad } from "@sveltejs/k
|
|||||||
export const load: ServerLoad = async ({ url, fetch }) => {
|
export const load: ServerLoad = async ({ url, fetch }) => {
|
||||||
const oauthError = url.searchParams.get('error');
|
const oauthError = url.searchParams.get('error');
|
||||||
if (oauthError) {
|
if (oauthError) {
|
||||||
|
// user cancelled
|
||||||
|
if(oauthError == "access_denied") {
|
||||||
|
return redirect(302, '/settings/integrations')
|
||||||
|
}
|
||||||
return error(400, {
|
return error(400, {
|
||||||
message: oauthError
|
message: oauthError
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{#if data.trail}
|
{#if data.trail}
|
||||||
<TrailInfoPanel
|
<TrailInfoPanel
|
||||||
activeTab={parseInt(page.url.searchParams.get("t") ?? "0")}
|
activeTab={parseInt(page.url.searchParams.get("t") ?? "0")}
|
||||||
trail={data.trail}
|
initTrail={data.trail}
|
||||||
mode="overview"
|
mode="overview"
|
||||||
></TrailInfoPanel>
|
></TrailInfoPanel>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user