mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-19 04:49:37 +02:00
Merge pull request #275 from seanmorley15/development
Merge for v0.6.0 update
This commit is contained in:
commit
030cacf4b1
19 changed files with 267 additions and 39 deletions
|
@ -13,7 +13,7 @@ WORKDIR /code
|
|||
|
||||
# Install system dependencies
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git postgresql-client \
|
||||
&& apt-get install -y git postgresql-client gdal-bin libgdal-dev \
|
||||
&& apt-get clean
|
||||
|
||||
# Install Python dependencies
|
||||
|
|
|
@ -13,6 +13,9 @@ done
|
|||
|
||||
>&2 echo "PostgreSQL is up - continuing..."
|
||||
|
||||
# run sql commands
|
||||
# psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -f /app/backend/init-postgis.sql
|
||||
|
||||
# Apply Django migrations
|
||||
python manage.py migrate
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ INSTALLED_APPS = (
|
|||
'adventures',
|
||||
'worldtravel',
|
||||
'users',
|
||||
# 'django_apscheduler',
|
||||
'django.contrib.gis',
|
||||
|
||||
)
|
||||
|
||||
|
@ -101,7 +101,7 @@ SIMPLE_JWT = {
|
|||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||
'NAME': getenv('PGDATABASE'),
|
||||
'USER': getenv('PGUSER'),
|
||||
'PASSWORD': getenv('PGPASSWORD'),
|
||||
|
@ -228,11 +228,9 @@ SWAGGER_SETTINGS = {
|
|||
'LOGOUT_URL': 'logout',
|
||||
}
|
||||
|
||||
|
||||
# For demo purposes only. Use a white list in the real world.
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
|
||||
from os import getenv
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
@ -262,5 +260,3 @@ LOGGING = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
SCHEDULER_AUTOSTART = True
|
|
@ -11,4 +11,4 @@ psycopg2-binary
|
|||
Pillow
|
||||
whitenoise
|
||||
django-resized
|
||||
django-apscheduler
|
||||
django-geojson
|
|
@ -4,7 +4,7 @@
|
|||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[1.9221462784913, 48.457599361977],
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"ISOCODE": "MX-CMX"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[-99.111241, 19.561498],
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"AWATER": 23736382213
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"type": "MultiPolygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[-94.0430515276176, 32.6930299766656],
|
||||
|
|
|
@ -4,11 +4,66 @@ from django.contrib.auth import get_user_model
|
|||
import requests
|
||||
from worldtravel.models import Country, Region
|
||||
from django.db import transaction
|
||||
from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon
|
||||
from django.contrib.gis.geos.error import GEOSException
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
media_root = settings.MEDIA_ROOT
|
||||
|
||||
|
||||
def setGeometry(region_code):
|
||||
# Assuming the file name is the country code (e.g., 'AU.json' for Australia)
|
||||
country_code = region_code.split('-')[0]
|
||||
json_file = os.path.join('static/data', f'{country_code.lower()}.json')
|
||||
|
||||
if not os.path.exists(json_file):
|
||||
print(f'File {country_code}.json does not exist (it probably hasn''t been added, contributors are welcome!)')
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(json_file, 'r') as f:
|
||||
geojson_data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Invalid JSON in file for {country_code}: {e}")
|
||||
return None
|
||||
|
||||
if 'type' not in geojson_data or geojson_data['type'] != 'FeatureCollection':
|
||||
print(f"Invalid GeoJSON structure for {country_code}: missing or incorrect 'type'")
|
||||
return None
|
||||
|
||||
if 'features' not in geojson_data or not geojson_data['features']:
|
||||
print(f"Invalid GeoJSON structure for {country_code}: missing or empty 'features'")
|
||||
return None
|
||||
|
||||
for feature in geojson_data['features']:
|
||||
try:
|
||||
properties = feature.get('properties', {})
|
||||
isocode = properties.get('ISOCODE')
|
||||
|
||||
if isocode == region_code:
|
||||
geometry = feature['geometry']
|
||||
geos_geom = GEOSGeometry(json.dumps(geometry))
|
||||
|
||||
if isinstance(geos_geom, Polygon):
|
||||
Region.objects.filter(id=region_code).update(geometry=MultiPolygon([geos_geom]))
|
||||
print(f"Updated geometry for region {region_code}")
|
||||
return MultiPolygon([geos_geom])
|
||||
elif isinstance(geos_geom, MultiPolygon):
|
||||
Region.objects.filter(id=region_code).update(geometry=geos_geom)
|
||||
print(f"Updated geometry for region {region_code}")
|
||||
return geos_geom
|
||||
else:
|
||||
print(f"Unexpected geometry type for region {region_code}: {type(geos_geom)}")
|
||||
return None
|
||||
|
||||
except (KeyError, ValueError, GEOSException) as e:
|
||||
print(f"Error processing region {region_code}: {e}")
|
||||
|
||||
print(f"No matching region found for {region_code}")
|
||||
return None
|
||||
|
||||
def saveCountryFlag(country_code):
|
||||
flags_dir = os.path.join(media_root, 'flags')
|
||||
|
||||
|
@ -616,7 +671,9 @@ class Command(BaseCommand):
|
|||
)
|
||||
if created:
|
||||
self.stdout.write(f'Inserted {name} into worldtravel regions')
|
||||
setGeometry(id)
|
||||
else:
|
||||
setGeometry(id)
|
||||
self.stdout.write(f'Updated {name} in worldtravel regions')
|
||||
|
||||
def insert_countries(self, countries):
|
||||
|
@ -627,6 +684,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
if created:
|
||||
saveCountryFlag(country_code)
|
||||
|
||||
self.stdout.write(f'Inserted {name} into worldtravel countries')
|
||||
else:
|
||||
saveCountryFlag(country_code)
|
||||
|
@ -641,5 +699,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
if created:
|
||||
self.stdout.write(f'Inserted {name} into worldtravel regions')
|
||||
setGeometry(id)
|
||||
else:
|
||||
setGeometry(id)
|
||||
self.stdout.write(f'{name} already exists in worldtravel regions')
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.0.8 on 2024-08-23 17:01
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('worldtravel', '0003_alter_region_name_en'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='country',
|
||||
name='geometry',
|
||||
field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0.8 on 2024-08-23 17:47
|
||||
|
||||
import django.contrib.gis.db.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('worldtravel', '0004_country_geometry'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='country',
|
||||
name='geometry',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='region',
|
||||
name='geometry',
|
||||
field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.gis.db import models as gis_models
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -35,6 +36,7 @@ class Country(models.Model):
|
|||
default=AFRICA
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Country"
|
||||
verbose_name_plural = "Countries"
|
||||
|
@ -47,6 +49,7 @@ class Region(models.Model):
|
|||
name = models.CharField(max_length=100)
|
||||
name_en = models.CharField(max_length=100, blank=True, null=True)
|
||||
country = models.ForeignKey(Country, on_delete=models.CASCADE)
|
||||
geometry = gis_models.MultiPolygonField(srid=4326, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -22,7 +22,7 @@ class RegionSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = Region
|
||||
fields = '__all__' # Serialize all fields of the Adventure model
|
||||
read_only_fields = ['id', 'name', 'country', 'name_en']
|
||||
read_only_fields = ['id', 'name', 'country', 'name_en', 'geometry']
|
||||
|
||||
class VisitedRegionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -8,8 +8,12 @@ from rest_framework.response import Response
|
|||
from rest_framework.decorators import api_view, permission_classes
|
||||
import os
|
||||
import json
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.conf import settings
|
||||
from rest_framework.decorators import action
|
||||
from django.contrib.staticfiles import finders
|
||||
from adventures.models import Adventure
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
|
@ -34,6 +38,39 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
|||
serializer_class = CountrySerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def check_point_in_region(self, request):
|
||||
lat = float(request.query_params.get('lat'))
|
||||
lon = float(request.query_params.get('lon'))
|
||||
point = Point(lon, lat, srid=4326)
|
||||
|
||||
region = Region.objects.filter(geometry__contains=point).first()
|
||||
|
||||
if region:
|
||||
return Response({'in_region': True, 'region_name': region.name, 'region_id': region.id})
|
||||
else:
|
||||
return Response({'in_region': False})
|
||||
|
||||
# make a post action that will get all of the users adventures and check if the point is in any of the regions if so make a visited region object for that user if it does not already exist
|
||||
@action(detail=False, methods=['post'])
|
||||
def region_check_all_adventures(self, request):
|
||||
adventures = Adventure.objects.filter(user_id=request.user.id, type='visited')
|
||||
count = 0
|
||||
for adventure in adventures:
|
||||
if adventure.latitude is not None and adventure.longitude is not None:
|
||||
try:
|
||||
print(f"Adventure {adventure.id}: lat={adventure.latitude}, lon={adventure.longitude}")
|
||||
point = Point(float(adventure.longitude), float(adventure.latitude), srid=4326)
|
||||
region = Region.objects.filter(geometry__contains=point).first()
|
||||
if region:
|
||||
if not VisitedRegion.objects.filter(user_id=request.user.id, region=region).exists():
|
||||
VisitedRegion.objects.create(user_id=request.user, region=region)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"Error processing adventure {adventure.id}: {e}")
|
||||
continue
|
||||
return Response({'regions_visited': count})
|
||||
|
||||
class RegionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
queryset = Region.objects.all()
|
||||
serializer_class = RegionSerializer
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# This script will create a backup of the adventurelog_media volume and store it in the current directory as adventurelog-backup.tar.gz
|
||||
|
||||
docker run --rm \
|
||||
-v adventurelog_adventurelog_media:/backup-volume \
|
||||
-v "$(pwd)":/backup \
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# This script is used to deploy the latest version of AdventureLog to the server. It pulls the latest version of the Docker images and starts the containers. It is a simple script that can be run on the server, possibly as a cron job, to keep the server up to date with the latest version of the application.
|
||||
|
||||
echo "Deploying latest version of AdventureLog"
|
||||
docker compose pull
|
||||
echo "Stating containers"
|
||||
|
|
|
@ -4,6 +4,7 @@ services:
|
|||
web:
|
||||
#build: ./frontend/
|
||||
image: ghcr.io/seanmorley15/adventurelog-frontend:latest
|
||||
container_name: adventurelog-frontend
|
||||
environment:
|
||||
- PUBLIC_SERVER_URL=http://server:8000 # MOST DOCKER USERS WILL NEVER NEED TO CHANGE THIS, EVEN IF YOU CHANGE THE PORTS
|
||||
- ORIGIN=http://localhost:8080
|
||||
|
@ -14,7 +15,8 @@ services:
|
|||
- server
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
image: postgis/postgis:15-3.3
|
||||
container_name: adventurelog-db
|
||||
environment:
|
||||
POSTGRES_DB: database
|
||||
POSTGRES_USER: adventure
|
||||
|
@ -25,6 +27,7 @@ services:
|
|||
server:
|
||||
#build: ./backend/
|
||||
image: ghcr.io/seanmorley15/adventurelog-backend:latest
|
||||
container_name: adventurelog-backend
|
||||
environment:
|
||||
- PGHOST=db
|
||||
- PGDATABASE=database
|
||||
|
@ -47,6 +50,7 @@ services:
|
|||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: adventurelog-nginx
|
||||
ports:
|
||||
- "81:80"
|
||||
volumes:
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
export let is_collection: boolean = false;
|
||||
|
||||
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||
let markers: Point[] = [];
|
||||
|
||||
let query: string = '';
|
||||
let places: OpenStreetMapPlace[] = [];
|
||||
let images: { id: string; image: string }[] = [];
|
||||
|
@ -29,6 +29,9 @@
|
|||
|
||||
let noPlaces: boolean = false;
|
||||
|
||||
let region_name: string | null = null;
|
||||
let region_id: string | null = null;
|
||||
|
||||
let adventure: Adventure = {
|
||||
id: '',
|
||||
name: '',
|
||||
|
@ -69,6 +72,8 @@
|
|||
collection: adventureToEdit?.collection || collection_id || null
|
||||
};
|
||||
|
||||
let markers: Point[] = [];
|
||||
|
||||
let url: string = '';
|
||||
let imageError: string = '';
|
||||
let wikiImageError: string = '';
|
||||
|
@ -76,6 +81,7 @@
|
|||
images = adventure.images || [];
|
||||
|
||||
if (adventure.longitude && adventure.latitude) {
|
||||
markers = [];
|
||||
markers = [
|
||||
{
|
||||
lngLat: { lng: adventure.longitude, lat: adventure.latitude },
|
||||
|
@ -84,6 +90,7 @@
|
|||
activity_type: ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}
|
||||
|
||||
if (longitude && latitude) {
|
||||
|
@ -98,6 +105,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
function clearMap() {
|
||||
console.log('CLEAR');
|
||||
markers = [];
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
|
||||
let imageSearch: string = adventure.name || '';
|
||||
|
||||
async function removeImage(id: string) {
|
||||
|
@ -132,6 +146,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (adventure.type != 'visited') {
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchImage() {
|
||||
let res = await fetch(url);
|
||||
let data = await res.blob();
|
||||
|
@ -237,6 +258,7 @@
|
|||
activity_type: data[0]?.type || ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}
|
||||
}
|
||||
console.log(data);
|
||||
|
@ -274,7 +296,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
function addMarker(e: CustomEvent<any>) {
|
||||
async function checkPointInRegion() {
|
||||
if (adventure.type == 'visited') {
|
||||
let lat = markers[0].lngLat.lat;
|
||||
let lon = markers[0].lngLat.lng;
|
||||
let res = await fetch(`/api/countries/check_point_in_region/?lat=${lat}&lon=${lon}`);
|
||||
let data = await res.json();
|
||||
if (data.error) {
|
||||
addToast('error', data.error);
|
||||
} else {
|
||||
if (data.in_region) {
|
||||
region_name = data.region_name;
|
||||
region_id = data.region_id;
|
||||
} else {
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addMarker(e: CustomEvent<any>) {
|
||||
markers = [];
|
||||
markers = [
|
||||
...markers,
|
||||
|
@ -285,6 +330,8 @@
|
|||
activity_type: ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
|
||||
console.log(markers);
|
||||
}
|
||||
|
||||
|
@ -308,6 +355,19 @@
|
|||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (region_id && region_name) {
|
||||
let res = await fetch(`/api/visitedregion/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ region: region_id })
|
||||
});
|
||||
if (res.ok) {
|
||||
addToast('success', `Region ${region_name} marked as visited`);
|
||||
}
|
||||
}
|
||||
|
||||
if (adventure.date && adventure.end_date) {
|
||||
if (new Date(adventure.date) > new Date(adventure.end_date)) {
|
||||
addToast('error', 'Start date must be before end date');
|
||||
|
@ -608,6 +668,9 @@
|
|||
bind:value={query}
|
||||
/>
|
||||
<button class="btn btn-neutral -mt-1" type="submit">Search</button>
|
||||
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
|
||||
>Clear Map</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
{#if places.length > 0}
|
||||
|
@ -628,6 +691,7 @@
|
|||
activity_type: place.type
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}}
|
||||
>
|
||||
{place.display_name}
|
||||
|
@ -655,6 +719,9 @@ it would also work to just use on:click on the MapLibre component itself. -->
|
|||
{/each}
|
||||
</MapLibre>
|
||||
</div>
|
||||
{#if region_name}
|
||||
<p class="text-lg font-semibold mt-2">Region: {region_name} ({region_id})</p>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">Save & Next</button>
|
||||
|
|
|
@ -19,21 +19,10 @@
|
|||
let showVisited = true;
|
||||
let showPlanned = true;
|
||||
|
||||
$: {
|
||||
if (!showVisited) {
|
||||
markers = data.props.markers.filter((marker) => marker.type !== 'visited');
|
||||
} else {
|
||||
const visitedMarkers = data.props.markers.filter((marker) => marker.type === 'visited');
|
||||
markers = [...markers, ...visitedMarkers];
|
||||
}
|
||||
if (!showPlanned) {
|
||||
markers = data.props.markers.filter((marker) => marker.type !== 'planned');
|
||||
} else {
|
||||
const plannedMarkers = data.props.markers.filter((marker) => marker.type === 'planned');
|
||||
markers = [...markers, ...plannedMarkers];
|
||||
}
|
||||
console.log(markers);
|
||||
}
|
||||
$: filteredMarkers = markers.filter(
|
||||
(marker) =>
|
||||
(showVisited && marker.type === 'visited') || (showPlanned && marker.type === 'planned')
|
||||
);
|
||||
|
||||
let newMarker = [];
|
||||
|
||||
|
@ -43,7 +32,6 @@
|
|||
function addMarker(e) {
|
||||
newMarker = [];
|
||||
newMarker = [...newMarker, { lngLat: e.detail.lngLat, name: 'Marker 1' }];
|
||||
console.log(newMarker);
|
||||
newLongitude = e.detail.lngLat.lng;
|
||||
newLatitude = e.detail.lngLat.lat;
|
||||
}
|
||||
|
@ -55,19 +43,15 @@
|
|||
}
|
||||
|
||||
function createNewAdventure(event) {
|
||||
console.log(event.detail);
|
||||
|
||||
let newMarker = {
|
||||
lngLat: [event.detail.longitude, event.detail.latitude],
|
||||
name: event.detail.name,
|
||||
type: 'planned'
|
||||
type: event.detail.type
|
||||
};
|
||||
markers = [...markers, newMarker];
|
||||
clearMarkers();
|
||||
console.log(markers);
|
||||
createModalOpen = false;
|
||||
}
|
||||
|
||||
let visitedRegions = data.props.visitedRegions;
|
||||
|
||||
let geoJSON = [];
|
||||
|
@ -154,7 +138,7 @@
|
|||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
||||
standardControls
|
||||
>
|
||||
{#each markers as { lngLat, name, type }}
|
||||
{#each filteredMarkers as { lngLat, name, type }}
|
||||
{#if type == 'visited'}
|
||||
<Marker
|
||||
{lngLat}
|
||||
|
|
|
@ -44,6 +44,21 @@
|
|||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function checkVisitedRegions() {
|
||||
let res = await fetch('/api/countries/region_check_all_adventures/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
let data = await res.json();
|
||||
if (res.ok) {
|
||||
addToast('success', `${data.regions_visited} regions updated`);
|
||||
} else {
|
||||
addToast('error', 'Error updating visited regions');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1 class="text-center font-extrabold text-4xl mb-6">Settings Page</h1>
|
||||
|
@ -152,7 +167,20 @@
|
|||
<button class="py-2 px-4 btn btn-primary mt-2">Change Email</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
|
||||
<div class="flex flex-col items-center mt-4">
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Visited Region Check</h1>
|
||||
<p>
|
||||
By selecting this, the server will check all of your visited adventures and mark the regions
|
||||
they are located in as "visited" in world travel.
|
||||
</p>
|
||||
<button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions}
|
||||
>Update Visited Regions</button
|
||||
>
|
||||
<p>This may take longer depending on the number of adventures you have.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center mt-4">
|
||||
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Data Export</h1>
|
||||
<button class="btn btn-neutral mb-4" on:click={exportAdventures}> Export to JSON </button>
|
||||
<p>This may take a few seconds...</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue