diff --git a/.env.example b/.env.example index 115daa7..89db7b2 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1 @@ -VITE_MAPBOX_KEY="MAPBOX_API_KEY" VITE_POCKETBASE_URL="http://localhost:8090" diff --git a/CHANGELOG.md b/CHANGELOG.md index 764ac79..ec587bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,30 @@ # Forager +## 2.1.0 + +### Minor Changes + +- 2f5dabb: Users can now create landmarks - Users are now able to create landmarks. This feature + re-purposes the arbitrary item menu to allow users to + add landmarks to the map. Landmarks, like items, can also be deleted. +- a021252: move calendar month component to images menu - Previously, all added items of interest could not have + their calendar months customised. You can now customise + items 'startMonth' and 'endMonth' months, the months + you can expect to find this item in the wild. Any existing + items you have will need to be manually edited. + +### Patch Changes + +- 68ed2d7: Include Docker deployment options - Forager can now be deployed with a Docker image +- bb3202a: Create loading screen on application login +- ad34d82: Add migration for default services, canCreateAccounts now defaults to true +- c03d753: Create user and item seeder + ## 2.0.0 ### Major Changes -- Move environment settings to user account - - Users will -now have to provide thier own Mapbox API keys on account creation (this is a breaking change). - - Users will now have to ensure the appropriate - Pocketbase server URL is set on first launch to - properly communicate with the server. - +- Move environment settings to user account - Users will + now have to provide thier own Mapbox API keys on account creation (this is a breaking change). - Users will now have to ensure the appropriate + Pocketbase server URL is set on first launch to + properly communicate with the server. diff --git a/Dockerfile b/Dockerfile index f0c9a2c..9c9864d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,19 @@ -FROM node:18.12.1 +FROM alpine:latest -LABEL author="Craig Broughton" -LABEL author.email="CRBroughton@posteo.uk" +ARG FORAGER_VERSION=2.0.1 -WORKDIR /app +RUN apk add --no-cache \ + unzip \ + ca-certificates -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +# download and unzip Forager +ADD https://github.com/CRBroughton/forager/releases/download/${FORAGER_VERSION}/forager-${FORAGER_VERSION}-linux.zip /tmp/forager.zip -ADD . . +RUN unzip /tmp/forager.zip -d forager +RUN cd forager && mv forager-${FORAGER_VERSION}-linux forager +RUN rm -rf /tmp/forager/zip -RUN npm i -g pnpm && pnpm i +EXPOSE 8080 -ENTRYPOINT ["/entrypoint.sh"] - -EXPOSE 4000 - -CMD ["npm", "run", "dev"] \ No newline at end of file +# start Forager +CMD ["forager/forager", "serve", "--http=0.0.0.0:8090"] \ No newline at end of file diff --git a/README.md b/README.md index 81e5da2..b8c8634 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,12 @@ This binary will provide both the back-end and front-end of the application. For deployment of the application, see [Pocketbases Going to Production documentation](https://pocketbase.io/docs/going-to-production/). +### Docker + +The Forager repository includes a `docker-compose.yml` file, enabling +quick deployment of Forager. Simply install Docker, clone the repository +and then run `docker compose up -d`. + ## Manual Installation Regardless of manual installation method, you will require the following to build Forager: @@ -114,6 +120,10 @@ Dependant on your operating system's architecture, download the latest release o into the db folder. When running `pnpm run pocketbase:serve` for the first time, the database migrations will ensure the correct tables are created. +If you wish to seed the database with a test user, one is provided +if you run `pnpm run pocketbase:seed` - You will need to update this +users password and Mapbox API key. + ## Progressive Web Application (PWA) Forager is a Progressive Web Application (PWA), and therefore can be installed via any browser, however requires an active connection to your [Pocketbase](https://github.com/pocketbase/pocketbase) instance. diff --git a/components.d.ts b/components.d.ts index de8097f..67d44bf 100644 --- a/components.d.ts +++ b/components.d.ts @@ -16,6 +16,7 @@ declare module 'vue' { ImageSettings: typeof import('./src/views/settings/ImageSettings.vue')['default'] InformationMark: typeof import('./src/components/InformationMark.vue')['default'] ItemDetails: typeof import('./src/components/ItemDetails.vue')['default'] + LoadingScreen: typeof import('./src/components/LoadingScreen.vue')['default'] LoginForm: typeof import('./src/components/LoginForm.vue')['default'] MonthSelector: typeof import('./src/components/MonthSelector.vue')['default'] ReferenceImage: typeof import('./src/components/ReferenceImage.vue')['default'] diff --git a/db/main.go b/db/main.go index dbfae13..9e350a8 100644 --- a/db/main.go +++ b/db/main.go @@ -13,6 +13,7 @@ import ( "github.com/pocketbase/pocketbase/plugins/migratecmd" _ "github.com/crbroughton/forager/migrations" + "github.com/crbroughton/forager/seeder" ) //go:embed all:dist @@ -21,6 +22,8 @@ var distDir embed.FS func main() { app := pocketbase.New() + seeder.AddSeederCommand(app) + // loosely check if it was executed using "go run" isGoRun := strings.HasPrefix(os.Args[0], os.TempDir()) diff --git a/db/migrations/1700070534_default_services.go b/db/migrations/1700070534_default_services.go new file mode 100644 index 0000000..c22bbc1 --- /dev/null +++ b/db/migrations/1700070534_default_services.go @@ -0,0 +1,50 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("Services") + if err != nil { + return err + } + + record := models.NewRecord(collection) + + record.Set("id", 1) + record.Set("canCreateAccounts", true) + + err = dao.SaveRecord(record) + if err != nil { + return err + } + + return nil + }, func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("Services") + if err != nil { + return err + } + + record, err := dao.FindRecordById(collection.Id, "1") + if err != nil { + return err + } + + err = dao.DeleteRecord(record) + if err != nil { + return err + } + + return nil + }) +} diff --git a/db/migrations/1700339939_landmarks_table.go b/db/migrations/1700339939_landmarks_table.go new file mode 100644 index 0000000..2ee0264 --- /dev/null +++ b/db/migrations/1700339939_landmarks_table.go @@ -0,0 +1,87 @@ +package migrations + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/models/schema" + "github.com/pocketbase/pocketbase/tools/types" +) + +func init() { + m.Register(func(db dbx.Builder) error { + dao := daos.New(db) + + ownerOnly := types.Pointer("@request.auth.id != '' && owner.id ?= @request.auth.id") + usersCollection, err := dao.FindCollectionByNameOrId("users") + if err != nil { + return err + } + + collection := &models.Collection{ + Name: "landmarks", + Type: models.CollectionTypeBase, + ListRule: ownerOnly, + ViewRule: ownerOnly, + CreateRule: types.Pointer(""), + UpdateRule: ownerOnly, + DeleteRule: ownerOnly, + System: false, + Schema: schema.NewSchema( + &schema.SchemaField{ + Name: "name", + Type: schema.FieldTypeText, + Required: true, + System: false, + }, + &schema.SchemaField{ + Name: "owner", + Type: schema.FieldTypeRelation, + Required: true, + System: false, + Options: &schema.RelationOptions{ + CollectionId: usersCollection.Id, + CascadeDelete: false, + MinSelect: types.Pointer(1), + MaxSelect: types.Pointer(1), + }, + }, + &schema.SchemaField{ + Name: "lng", + Type: schema.FieldTypeNumber, + Required: true, + System: false, + }, + &schema.SchemaField{ + Name: "lat", + Type: schema.FieldTypeNumber, + Required: true, + System: false, + }, + ), + } + + err = dao.SaveCollection(collection) + + if err != nil { + return err + } + + return nil + }, func(db dbx.Builder) error { + dao := daos.New(db) + + collection, err := dao.FindCollectionByNameOrId("landmarks") + if err != nil { + return err + } + + err = dao.DeleteCollection(collection) + if err != nil { + return err + } + + return nil + }) +} diff --git a/db/seeder/seeder.go b/db/seeder/seeder.go new file mode 100644 index 0000000..8c5cdd7 --- /dev/null +++ b/db/seeder/seeder.go @@ -0,0 +1,241 @@ +package seeder + +import ( + "encoding/json" + "log" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/models" + "github.com/spf13/cobra" +) + +func AddSeederCommand(app *pocketbase.PocketBase) { + app.RootCmd.AddCommand(&cobra.Command{ + Use: "seed", + Short: "Seeds the database with a default admin user, item markers and landmarks", + Run: func(cmd *cobra.Command, args []string) { + seedTestUser(app) + seedTestUsersMarkers(app) + seedTestUsersLandmarks(app) + }, + }) +} + +func seedTestUsersLandmarks(app *pocketbase.PocketBase) { + db := app.Dao() + + collection, err := db.FindCollectionByNameOrId("landmarks") + if err != nil { + log.Fatal(err) + } + + type item struct { + Id int `json:"id"` + Name string `json:"name"` + Owner int `json:"owner"` + Lng string `json:"lng"` + Lat string `json:"lat"` + } + + items := []item{ + { + Id: 1, + Name: "The Level", + Owner: 1, + Lng: "-0.13308322562591002", + Lat: "50.83105275081468", + }, + } + + app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + for _, item := range items { + record := models.NewRecord(collection) + + record.Set("id", item.Id) + record.Set("name", item.Name) + record.Set("owner", item.Owner) + record.Set("lng", item.Lng) + record.Set("lat", item.Lat) + + err = txDao.Save(record) + if err != nil { + log.Fatal(err) + } + + } + return nil + }) + +} + +func seedTestUsersMarkers(app *pocketbase.PocketBase) { + db := app.Dao() + + collection, err := db.FindCollectionByNameOrId("items") + if err != nil { + log.Fatal(err) + } + + type item struct { + Id int `json:"id"` + Name string `json:"name"` + Date string `json:"date"` + LastForaged string `json:"lastForaged"` + Owner int `json:"owner"` + Lng string `json:"lng"` + Lat string `json:"lat"` + Colour string `json:"colour"` + StartMonth string `json:"startMonth"` + EndMonth string `json:"endMonth"` + ImageURL string `json:"imageURL"` + } + + items := []item{ + { + Id: 1, + Name: "Blackberries", + Date: "2023-11-14 19:23:57", + Owner: 1, + Lng: "-0.13107839490911033", + Lat: "50.8315128991386", + Colour: "purple", + StartMonth: "August", + EndMonth: "September", + ImageURL: "https://upload.wikimedia.org/wikipedia/commons/7/78/Ripe%2C_ripening%2C_and_green_blackberries.jpg", + }, + { + Id: 2, + Name: "Hawthorn", + Date: "2023-11-14 19:23:58", + Owner: 1, + Lng: "-0.13254663509749776", + Lat: "50.8304424332006", + Colour: "deeppink", + StartMonth: "January", + EndMonth: "December", + ImageURL: "https://images.immediate.co.uk/production/volatile/sites/23/2019/09/GettyImages-513147101-hawthorn-Neil-Holmes-dba76ab.jpg", + }, + { + Id: 3, + Name: "Elderberries", + Date: "2023-11-14 19:23:60", + Owner: 1, + Lng: "-0.13364328363357458", + Lat: "50.83169035368675", + Colour: "cadetblue", + StartMonth: "January", + EndMonth: "December", + ImageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Sambucus-berries.jpg/1280px-Sambucus-berries.jpg", + }, + { + Id: 4, + Name: "Rhubarb", + Date: "2023-11-14 19:23:12", + Owner: 1, + Lng: "-0.13408738097555783", + Lat: "50.83086604297725", + Colour: "purple", + StartMonth: "January", + EndMonth: "December", + ImageURL: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Rheum_rhabarbarum.2006-04-27.uellue.jpg/1280px-Rheum_rhabarbarum.2006-04-27.uellue.jpg", + }, + } + + app.Dao().RunInTransaction(func(txDao *daos.Dao) error { + for _, item := range items { + record := models.NewRecord(collection) + + record.Set("id", item.Id) + record.Set("name", item.Name) + record.Set("date", item.Date) + record.Set("owner", item.Owner) + record.Set("lastForaged", item.LastForaged) + record.Set("lng", item.Lng) + record.Set("lat", item.Lat) + record.Set("colour", item.Colour) + record.Set("startMonth", item.StartMonth) + record.Set("endMonth", item.EndMonth) + record.Set("imageURL", item.ImageURL) + + err = txDao.Save(record) + if err != nil { + log.Fatal(err) + } + + } + return nil + }) + +} + +func seedTestUser(app *pocketbase.PocketBase) { + db := app.Dao() + + collection, err := db.FindCollectionByNameOrId("users") + if err != nil { + log.Fatal(err) + } + + record := models.NewRecord(collection) + + type image struct { + Colour string `json:"colour"` + Name string `json:"name"` + Url string `json:"url"` + StartMonth string `json:"startMonth"` + EndMonth string `json:"endMonth"` + } + + images := []image{ + { + Colour: "purple", + Name: "Blackberries", + Url: "https://upload.wikimedia.org/wikipedia/commons/7/78/Ripe%2C_ripening%2C_and_green_blackberries.jpg", + StartMonth: "August", + EndMonth: "September", + }, + { + Colour: "deeppink", + Name: "Hawthorn", + Url: "https://images.immediate.co.uk/production/volatile/sites/23/2019/09/GettyImages-513147101-hawthorn-Neil-Holmes-dba76ab.jpg", + StartMonth: "January", + EndMonth: "December", + }, + { + Name: "Rhubarb", + Url: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2b/Rheum_rhabarbarum.2006-04-27.uellue.jpg/1280px-Rheum_rhabarbarum.2006-04-27.uellue.jpg", + StartMonth: "January", + EndMonth: "December", + }, + { + Colour: "cadetblue", + Name: "Elderberries", + Url: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Sambucus-berries.jpg/1280px-Sambucus-berries.jpg", + StartMonth: "January", + EndMonth: "December", + }, + } + + JSON, err := json.MarshalIndent(images, "", " ") + if err != nil { + log.Fatal(err) + } + + log.Default().Println(JSON, images) + + record.Set("id", 1) + record.Set("name", "testuser") + record.Set("username", "testuser") + record.Set("password", "testuser") + record.Set("lng", -0.13309169393016873) + record.Set("lat", 50.83106120191778) + record.Set("disclaimerAgreed", true) + record.Set("tokenKey", 1) + record.Set("images", JSON) + + err = db.SaveRecord(record) + if err != nil { + log.Fatal(err) + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 59b3247..15d0ef1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,14 @@ -version: '3.8' +version: "3.8" services: - ui: - container_name: vite-ui - build: - context: ./ - dockerfile: Dockerfile + forager: + build: . + container_name: forager + environment: + - PUID=1000 + - PGID=1000 volumes: - - ./:/app - - ./node_modules:/app/node_modules + - ./forager:/pb/pb_data ports: - - '4000:4000' + - 8090:8090 restart: unless-stopped \ No newline at end of file diff --git a/package.json b/package.json index b434436..7bfcaa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "forager", - "version": "2.0.0", + "version": "2.1.0", "scripts": { "dev": "vite --host", "build": "vue-tsc --noEmit && vite build", @@ -10,8 +10,12 @@ "lint:check": "eslint \"src/*.{vue,ts,js}\"", "lint:fix": "eslint \"src/*.{vue,ts,js}\" --fix", "serve": "vite preview", - "pocketbase:serve": "./db/pocketbase serve", + "pocketbase:serve": "cd db && go run main.go serve", + "pocketbase:seed": "cd db && go run main.go seed", "pocketbase:types": "npx pocketbase-typegen --db ./db/pb_data/data.db --out ./src/pocketbase-types.ts", + "pocketbase:migrate": "cd db && go run main.go migrate", + "pocketbase:migrate:down": "cd db && go run main.go migrate down", + "pocketbase:migrate:create": "cd db && go run main.go migrate create", "changeset": "npx changeset", "changeset:status": "npx changeset status --verbose", "changeset:version": "npx changeset version" diff --git a/src/App.vue b/src/App.vue index 9d7fe4d..82a91b8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -93,6 +93,7 @@ const settingsMenuVisible = ref(false)
+
diff --git a/src/components/AddMarker.vue b/src/components/AddMarker.vue index 9f471a8..231471a 100644 --- a/src/components/AddMarker.vue +++ b/src/components/AddMarker.vue @@ -22,32 +22,47 @@ watch(() => createItemRef.value, () => { if (createItemRef.value === null) return - createItemPopup('#newItem', 'Creates a new item, either from your images, or an arbitrary item') + createItemPopup('#newItem', 'Create a new item, or a new landmark') }) const mapboxStore = useMapbox() const { lng, lat } = storeToRefs(mapboxStore) const pocketbaseStore = usePocketBase() const { user } = storeToRefs(pocketbaseStore) -const selectedStartMonth = ref('January') -const selectedEndMonth = ref('December') -const creatingNewItem = ref(false) +const createLandmark = ref(false) + +const selectedItem = reactive({ + name: '', + colour: '', + startMonth: '', + endMonth: '', + url: '', +}) const input = ref('') function hide() { input.value = '' - creatingNewItem.value = false + createLandmark.value = false emits('hide') } -const imageURL = ref('') -const selectedColour = ref('red') - function setSelectedItem(event: UserImage) { - imageURL.value = event.url - input.value = event.name - selectedColour.value = event.colour + Object.assign(selectedItem, event) +} + +function create() { + if (!createLandmark.value) { + mapboxStore.addMarker(lng.value, lat.value, selectedItem) + return + } + + mapboxStore.addLandmark({ + name: input.value, + lng: lng.value, + lat: lat.value, + }) + } @@ -55,41 +70,39 @@ function setSelectedItem(event: UserImage) {
-

Create new item

+

{{ !createLandmark ? 'Create new item' : 'Create landmark' }}

-
+
+

+ back +

-
- -
+
+ +
-
- +
+ -
- + Create diff --git a/src/components/ItemDetails.vue b/src/components/ItemDetails.vue index 37d4bae..bd2caa7 100644 --- a/src/components/ItemDetails.vue +++ b/src/components/ItemDetails.vue @@ -3,14 +3,14 @@ import { usePocketBase } from '@/pocketbase' import { useMapbox } from '@/mapbox' const mapboxStore = useMapbox() -const { selectedItem } = storeToRefs(mapboxStore) +const { selectedItem, selectedCollection } = storeToRefs(mapboxStore) const pocketbaseStore = usePocketBase() const { selectedItemPocketbase } = storeToRefs(pocketbaseStore) watch(() => selectedItem.value, () => { if (selectedItem.value !== undefined) - pocketbaseStore.getSelectedItem(selectedItem.value) + pocketbaseStore.getSelectedItem(selectedItem.value, selectedCollection.value) }) function clearSelected() { @@ -51,7 +51,7 @@ const previewImg = computed(() => {
-
+

Last Foraged: {{ selectedItemPocketbase.lastForaged ? new Date(selectedItemPocketbase.lastForaged!).toDateString() : 'Never' }}

Start Month: {{ selectedItemPocketbase.startMonth || 'Not Set' }}

End Month: {{ selectedItemPocketbase.endMonth || 'Not Set' }}

@@ -60,7 +60,7 @@ const previewImg = computed(() => { Delete - + Forage Now diff --git a/src/components/LoadingScreen.vue b/src/components/LoadingScreen.vue new file mode 100644 index 0000000..7bb7160 --- /dev/null +++ b/src/components/LoadingScreen.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/src/components/ReferenceImage.vue b/src/components/ReferenceImage.vue index e29f47c..f52f549 100644 --- a/src/components/ReferenceImage.vue +++ b/src/components/ReferenceImage.vue @@ -16,8 +16,8 @@ const { isLoading } = useImage({ src: props.image.url })