adds email notifications
This commit is contained in:
12
db/main.go
12
db/main.go
@@ -114,7 +114,7 @@ func createTrailHandler(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
|||||||
if e.Record.GetBool("public") {
|
if e.Record.GetBool("public") {
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.TrailCreate,
|
Type: util.TrailCreate,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"id": e.Record.Id,
|
"id": e.Record.Id,
|
||||||
"trail": e.Record.GetString("name"),
|
"trail": e.Record.GetString("name"),
|
||||||
},
|
},
|
||||||
@@ -163,7 +163,7 @@ func createTrailShareHandler(app *pocketbase.PocketBase, client meilisearch.Serv
|
|||||||
|
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.TrailShare,
|
Type: util.TrailShare,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"id": shareTrail.Id,
|
"id": shareTrail.Id,
|
||||||
"trail": shareTrail.GetString("name"),
|
"trail": shareTrail.GetString("name"),
|
||||||
"author": shareTrailAuthor.GetString("username"),
|
"author": shareTrailAuthor.GetString("username"),
|
||||||
@@ -185,7 +185,7 @@ func createListShareHandler(app *pocketbase.PocketBase) func(e *core.RecordCreat
|
|||||||
|
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.ListShare,
|
Type: util.ListShare,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"id": shareList.Id,
|
"id": shareList.Id,
|
||||||
"list": shareList.GetString("name"),
|
"list": shareList.GetString("name"),
|
||||||
"author": shareListAuthor.GetString("username"),
|
"author": shareListAuthor.GetString("username"),
|
||||||
@@ -211,7 +211,7 @@ func createListHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateEven
|
|||||||
}
|
}
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.ListCreate,
|
Type: util.ListCreate,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"id": e.Record.Id,
|
"id": e.Record.Id,
|
||||||
"list": e.Record.GetString("name"),
|
"list": e.Record.GetString("name"),
|
||||||
},
|
},
|
||||||
@@ -231,7 +231,7 @@ func createFollowHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateEv
|
|||||||
|
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.NewFollower,
|
Type: util.NewFollower,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"follower": follower.GetString("username"),
|
"follower": follower.GetString("username"),
|
||||||
},
|
},
|
||||||
Seen: false,
|
Seen: false,
|
||||||
@@ -252,7 +252,7 @@ func createCommentHandler(app *pocketbase.PocketBase) func(e *core.RecordCreateE
|
|||||||
|
|
||||||
notification := util.Notification{
|
notification := util.Notification{
|
||||||
Type: util.TrailComment,
|
Type: util.TrailComment,
|
||||||
Metadata: map[string]interface{}{
|
Metadata: map[string]string{
|
||||||
"id": commentTrail.Id,
|
"id": commentTrail.Id,
|
||||||
"author": commentAuthor.GetString("username"),
|
"author": commentAuthor.GetString("username"),
|
||||||
"trail": commentTrail.GetString("name"),
|
"trail": commentTrail.GetString("name"),
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func init() {
|
|||||||
|
|
||||||
return daos.New(db).SaveCollection(collection)
|
return daos.New(db).SaveCollection(collection)
|
||||||
}, func(db dbx.Builder) error {
|
}, func(db dbx.Builder) error {
|
||||||
dao := daos.New(db);
|
dao := daos.New(db)
|
||||||
|
|
||||||
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
collection, err := dao.FindCollectionByNameOrId("urytyc428mwlbqq")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
76
db/migrations/1735935107_updated_lists.go
Normal file
76
db/migrations/1735935107_updated_lists.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
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("r6gu2ajyidy1x69")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_author := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "kwm6zdet",
|
||||||
|
"name": "author",
|
||||||
|
"type": "relation",
|
||||||
|
"required": true,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}`), edit_author); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_author)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("r6gu2ajyidy1x69")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_author := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "kwm6zdet",
|
||||||
|
"name": "author",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": 1,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}`), edit_author); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_author)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
122
db/templates/mail/notification.html
Normal file
122
db/templates/mail/notification.html
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
margin: 20px auto;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 600px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background-color: #242734;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header img {
|
||||||
|
max-width: 150px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greeting,
|
||||||
|
.goodbye {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
background: #f1f1f1;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #242734;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #777777;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #242734;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="header">
|
||||||
|
<img src="{{.AppUrl}}/imgs/logo_text_light.png" alt="wanderer" />
|
||||||
|
</div>
|
||||||
|
<!-- Greeting -->
|
||||||
|
<div class="greeting">
|
||||||
|
<p>Hello {{.RecipientName}},</p>
|
||||||
|
<p>You have a new notification:</p>
|
||||||
|
</div>
|
||||||
|
<!-- Dynamic Content -->
|
||||||
|
<div class="content">
|
||||||
|
{{.Content}}
|
||||||
|
<div class="">
|
||||||
|
<a class="button" href="{{.AppUrl}}">
|
||||||
|
Check it out!
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="goodbye">
|
||||||
|
<p>Kind regards,</p>
|
||||||
|
<p>Your wanderer team</p>
|
||||||
|
</div>
|
||||||
|
<!-- Footer Section -->
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 wanderer | <a href="{{.AppUrl}}/settings/notifications">Unsubscribe</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
67
db/util/email_templates.go
Normal file
67
db/util/email_templates.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
t "html/template"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/tools/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailData struct {
|
||||||
|
AppUrl string
|
||||||
|
RecipientName string
|
||||||
|
Content t.HTML
|
||||||
|
Link string
|
||||||
|
FollowerName string
|
||||||
|
}
|
||||||
|
|
||||||
|
var notificationTemplates = map[NotificationType]string{
|
||||||
|
TrailCreate: "{{.Author}} has created a new trail: {{.trail}}.",
|
||||||
|
TrailShare: "{{.Author}} has shared a trail with you: {{.trail}}.",
|
||||||
|
ListCreate: "{{.Author}} has created a new list: {{.list}}.",
|
||||||
|
ListShare: "{{.Author}} has shared a list with you: {{.list}}.",
|
||||||
|
NewFollower: "Good news! You have a new follower: {{.Author}}.",
|
||||||
|
TrailComment: "{{.Author}} commented on your trail '{{.trail}}': '{{.comment}}'.",
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateHTML(appUrl string, recipientName string, authorName string, notificationType NotificationType, metadata map[string]string) (string, error) {
|
||||||
|
registry := template.NewRegistry()
|
||||||
|
|
||||||
|
// Get custom content template for the notification type
|
||||||
|
customTemplate, exists := notificationTemplates[notificationType]
|
||||||
|
if !exists {
|
||||||
|
return "", errors.New("unknown notification type")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create content template with dynamic data
|
||||||
|
contentTmpl, err := t.New("content").Parse(customTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the custom content with data
|
||||||
|
metadata["Author"] = authorName
|
||||||
|
var contentBuffer bytes.Buffer
|
||||||
|
err = contentTmpl.Execute(&contentBuffer, metadata)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the final email with boilerplate and custom content
|
||||||
|
content := EmailData{
|
||||||
|
AppUrl: appUrl,
|
||||||
|
RecipientName: recipientName,
|
||||||
|
Content: t.HTML(contentBuffer.String()),
|
||||||
|
}
|
||||||
|
|
||||||
|
html, err := registry.LoadFiles(
|
||||||
|
"templates/mail/notification.html",
|
||||||
|
).Render(content)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return html, nil
|
||||||
|
}
|
||||||
@@ -2,10 +2,12 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase"
|
"github.com/pocketbase/pocketbase"
|
||||||
"github.com/pocketbase/pocketbase/models"
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/mailer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationType string
|
type NotificationType string
|
||||||
@@ -21,7 +23,7 @@ const (
|
|||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
Type NotificationType `json:"type"`
|
Type NotificationType `json:"type"`
|
||||||
Metadata interface{} `json:"metadata,omitempty"`
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
Seen bool `json:"seen"`
|
Seen bool `json:"seen"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
}
|
}
|
||||||
@@ -59,13 +61,13 @@ func SendNotification(app *pocketbase.PocketBase, notification Notification, rec
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !permissions.Web {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
notifications, err := app.Dao().FindCollectionByNameOrId("notifications")
|
notifications, err := app.Dao().FindCollectionByNameOrId("notifications")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if permissions.Web {
|
||||||
n := models.NewRecord(notifications)
|
n := models.NewRecord(notifications)
|
||||||
n.Set("type", string(notification.Type))
|
n.Set("type", string(notification.Type))
|
||||||
n.Set("metadata", notification.Metadata)
|
n.Set("metadata", notification.Metadata)
|
||||||
@@ -76,6 +78,34 @@ func SendNotification(app *pocketbase.PocketBase, notification Notification, rec
|
|||||||
if err := app.Dao().SaveRecord(n); err != nil {
|
if err := app.Dao().SaveRecord(n); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if permissions.Email {
|
||||||
|
recipientUser, err := app.Dao().FindRecordById("users", recipient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
authorUser, err := app.Dao().FindRecordById("users", notification.Author)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
html, err := GenerateHTML(app.Settings().Meta.AppUrl, recipientUser.Username(), authorUser.Username(), notification.Type, notification.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
message := &mailer.Message{
|
||||||
|
From: mail.Address{
|
||||||
|
Address: app.Settings().Meta.SenderAddress,
|
||||||
|
Name: app.Settings().Meta.SenderName,
|
||||||
|
},
|
||||||
|
To: []mail.Address{{Address: recipientUser.Email()}},
|
||||||
|
Subject: "wanderer - New Notification",
|
||||||
|
HTML: html,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.NewMailClient().Send(message)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,32 +115,11 @@ func SendNotificationToFollowers(app *pocketbase.PocketBase, notification Notifi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
notifications, err := app.Dao().FindCollectionByNameOrId("notifications")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, f := range followers {
|
for _, f := range followers {
|
||||||
recipient := f.GetString("follower")
|
recipient := f.GetString("follower")
|
||||||
if notification.Author == recipient {
|
SendNotification(app, notification, recipient)
|
||||||
continue
|
|
||||||
}
|
|
||||||
permissions, err := getNotificationPermissions(app, recipient, notification.Type)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !permissions.Web {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n := models.NewRecord(notifications)
|
|
||||||
n.Set("type", string(notification.Type))
|
|
||||||
n.Set("metadata", notification.Metadata)
|
|
||||||
n.Set("seen", notification.Seen)
|
|
||||||
n.Set("recipient", recipient)
|
|
||||||
n.Set("author", notification.Author)
|
|
||||||
|
|
||||||
if err := app.Dao().SaveRecord(n); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,10 @@ To update all containers to a new version simply run `docker compose pull && doc
|
|||||||
While not recommended it is absolutely possible to install wanderer from source.
|
While not recommended it is absolutely possible to install wanderer from source.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
1. git installed && `git clone https://github.com/Flomp/wanderer.git`
|
1. git installed && `git clone https://github.com/Flomp/wanderer.git --branch v0.12.0 --single-branch`
|
||||||
2. go >= 1.21.1 installed
|
2. go >= 1.21.1 installed
|
||||||
3. node >= 18.17.0 installed
|
3. node >= 18.17.0 installed
|
||||||
|
4. npm >= 8.15.0 installed
|
||||||
|
|
||||||
### meilisearch
|
### meilisearch
|
||||||
wanderer uses meilisearch without any further modifications. As a result, you can simply head over to [their website](https://www.meilisearch.com/docs/learn/getting_started/installation) and follow the instructions for your preferred platform. We assume that you put the binary in the `wanderer/search` directory. If you did not, adapt the launch script below accordingly.
|
wanderer uses meilisearch without any further modifications. As a result, you can simply head over to [their website](https://www.meilisearch.com/docs/learn/getting_started/installation) and follow the instructions for your preferred platform. We assume that you put the binary in the `wanderer/search` directory. If you did not, adapt the launch script below accordingly.
|
||||||
@@ -157,15 +158,26 @@ Caution: Ensure that you change the `MEILI_MASTER_KEY` to a different value if y
|
|||||||
```bash
|
```bash
|
||||||
trap "kill 0" EXIT
|
trap "kill 0" EXIT
|
||||||
|
|
||||||
|
# learn more about the configuration:
|
||||||
|
# https://wanderer.to/getting-started/configuration/
|
||||||
|
# required
|
||||||
export ORIGIN=http://localhost:3000
|
export ORIGIN=http://localhost:3000
|
||||||
export MEILI_URL=http://127.0.0.1:7700
|
export MEILI_URL=http://127.0.0.1:7700
|
||||||
export MEILI_MASTER_KEY=CHANGE_ME!
|
export MEILI_MASTER_KEY=YOU_SHOULD_DEFINITELY_CHANGE_ME
|
||||||
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
export PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090
|
||||||
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
|
export PUBLIC_VALHALLA_URL=https://valhalla1.openstreetmap.de
|
||||||
|
|
||||||
|
# optional
|
||||||
|
# export MEILI_NO_ANALYTICS=true
|
||||||
|
# export BODY_SIZE_LIMIT=Infinity
|
||||||
|
# export PUBLIC_DISABLE_SIGNUP=false
|
||||||
|
# export UPLOAD_FOLDER=/app/uploads
|
||||||
|
# export UPLOAD_USER=
|
||||||
|
# export UPLOAD_PASSWORD=
|
||||||
|
|
||||||
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
cd search && ./meilisearch --master-key $MEILI_MASTER_KEY &
|
||||||
cd db && ./pocketbase serve &
|
cd db && ./pocketbase serve &
|
||||||
cd web/build && node build &
|
cd web && node build &
|
||||||
|
|
||||||
wait
|
wait
|
||||||
```
|
```
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -140,7 +140,7 @@
|
|||||||
|
|
||||||
const r: GeoJSON[] = [];
|
const r: GeoJSON[] = [];
|
||||||
trails.forEach((t) => {
|
trails.forEach((t) => {
|
||||||
if (t.expand.gpx_data) {
|
if (t.expand?.gpx_data) {
|
||||||
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
|
r.push(toGeoJson(t.expand.gpx_data) as GeoJSON);
|
||||||
} else if (t.lat && t.lon) {
|
} else if (t.lat && t.lon) {
|
||||||
r.push({
|
r.push({
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
if (data[activeTrail] && showElevation) {
|
if (data[activeTrail] && showElevation) {
|
||||||
epc?.setData(
|
epc?.setData(
|
||||||
data[activeTrail]!,
|
data[activeTrail]!,
|
||||||
trails.at(activeTrail)!.expand.waypoints,
|
trails.at(activeTrail)!.expand?.waypoints,
|
||||||
);
|
);
|
||||||
epc?.showProfile();
|
epc?.showProfile();
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@
|
|||||||
if (data[activeTrail] && showElevation) {
|
if (data[activeTrail] && showElevation) {
|
||||||
epc?.setData(
|
epc?.setData(
|
||||||
data[activeTrail]!,
|
data[activeTrail]!,
|
||||||
trails.at(activeTrail)!.expand.waypoints,
|
trails.at(activeTrail)!.expand?.waypoints,
|
||||||
);
|
);
|
||||||
epc?.showProfile();
|
epc?.showProfile();
|
||||||
}
|
}
|
||||||
@@ -490,7 +490,7 @@
|
|||||||
if (!map) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const waypoint of trails[activeTrail]?.expand.waypoints ?? []) {
|
for (const waypoint of trails[activeTrail]?.expand?.waypoints ?? []) {
|
||||||
const marker = createMarkerFromWaypoint(waypoint, onMarkerDragEnd);
|
const marker = createMarkerFromWaypoint(waypoint, onMarkerDragEnd);
|
||||||
marker.addTo(map);
|
marker.addTo(map);
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const RecordOptionsSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const RecordListOptionsSchema = RecordOptionsSchema.extend({
|
const RecordListOptionsSchema = RecordOptionsSchema.extend({
|
||||||
page: z.number({ coerce: true }).int().positive().optional(),
|
page: z.number({ coerce: true }).int().nonnegative().optional(),
|
||||||
perPage: z.number({ coerce: true }).int().optional(),
|
perPage: z.number({ coerce: true }).int().optional(),
|
||||||
sort: z.string().optional(),
|
sort: z.string().optional(),
|
||||||
filter: z.string().optional(),
|
filter: z.string().optional(),
|
||||||
|
|||||||
@@ -132,6 +132,15 @@
|
|||||||
|
|
||||||
function handleMapInit() {
|
function handleMapInit() {
|
||||||
if (
|
if (
|
||||||
|
$page.url.searchParams.has("lat") &&
|
||||||
|
$page.url.searchParams.has("lon")
|
||||||
|
) {
|
||||||
|
const lat = $page.url.searchParams.get("lat");
|
||||||
|
const lon = $page.url.searchParams.get("lon");
|
||||||
|
map.setCenter([parseFloat(lon!), parseFloat(lat!)]);
|
||||||
|
map.setZoom(14);
|
||||||
|
console.log("Set map center to: " + lat + " " + lon);
|
||||||
|
} else if (
|
||||||
$page.url.searchParams.has("tl_lat") &&
|
$page.url.searchParams.has("tl_lat") &&
|
||||||
$page.url.searchParams.has("tl_lon") &&
|
$page.url.searchParams.has("tl_lon") &&
|
||||||
$page.url.searchParams.has("br_lat") &&
|
$page.url.searchParams.has("br_lat") &&
|
||||||
@@ -148,14 +157,6 @@
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
map.fitBounds(boundingBox, { animate: false });
|
map.fitBounds(boundingBox, { animate: false });
|
||||||
} else if (
|
|
||||||
$page.url.searchParams.has("lat") &&
|
|
||||||
$page.url.searchParams.has("lon")
|
|
||||||
) {
|
|
||||||
const lat = $page.url.searchParams.get("lat");
|
|
||||||
const lon = $page.url.searchParams.get("lon");
|
|
||||||
map.setCenter([parseFloat(lon!), parseFloat(lat!)]);
|
|
||||||
map.setZoom(14);
|
|
||||||
} else if (settings && settings.mapFocus == "trails") {
|
} else if (settings && settings.mapFocus == "trails") {
|
||||||
const boundingBox: M.LngLatBoundsLike = [
|
const boundingBox: M.LngLatBoundsLike = [
|
||||||
[maxBoundingBox.min_lon, maxBoundingBox.max_lat],
|
[maxBoundingBox.min_lon, maxBoundingBox.max_lat],
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
text: $_("trail", { values: { n: 2 } }),
|
text: $_("trail", { values: { n: 2 } }),
|
||||||
value: `/profile/${$page.params.id}/trails`,
|
value: `/profile/${$page.params.id}/trails`,
|
||||||
},
|
},
|
||||||
{ text: $_('statistics'), value: `/profile/${$page.params.id}/stats` },
|
{ text: $_("statistics"), value: `/profile/${$page.params.id}/stats` },
|
||||||
];
|
];
|
||||||
|
|
||||||
$: activeIndex = profileLinks.findIndex(
|
$: activeIndex = profileLinks.findIndex(
|
||||||
@@ -80,14 +80,14 @@
|
|||||||
href="/profile/{data.user.id}/users/followers"
|
href="/profile/{data.user.id}/users/followers"
|
||||||
>
|
>
|
||||||
<p class="font-semibold">{data.followers}</p>
|
<p class="font-semibold">{data.followers}</p>
|
||||||
<p>{$_('followers')}</p>
|
<p>{$_("followers")}</p>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class:font-bold={$page.url.pathname.endsWith("following")}
|
class:font-bold={$page.url.pathname.endsWith("following")}
|
||||||
href="/profile/{data.user.id}/users/following"
|
href="/profile/{data.user.id}/users/following"
|
||||||
>
|
>
|
||||||
<p class="font-semibold">{data.following}</p>
|
<p class="font-semibold">{data.following}</p>
|
||||||
<p>{$_('following')}</p>
|
<p>{$_("following")}</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#if !data.isOwnProfile}
|
{#if !data.isOwnProfile}
|
||||||
@@ -113,6 +113,12 @@
|
|||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if data.isOwnProfile}
|
||||||
|
<div class="px-6 mb-4 flex flex-col gap-2">
|
||||||
|
<button class="btn-secondary basis-full">Share profile</button>
|
||||||
|
<a class="btn-secondary text-center basis-full" href="/settings/profile">{$_("settings")}</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
$_("must-be-at-least-n-characters-long", {
|
$_("must-be-at-least-n-characters-long", {
|
||||||
values: { n: 3 },
|
values: { n: 3 },
|
||||||
}),
|
}),
|
||||||
),
|
).regex(/^[\w][\w\.]*$/, $_("invalid-username")),
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "required")
|
.min(1, "required")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
web/static/imgs/logo_text_light.png
Normal file
BIN
web/static/imgs/logo_text_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Reference in New Issue
Block a user