mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 07:19:36 +02:00
commit
01cdcf67a2
23 changed files with 882 additions and 531 deletions
2
.github/workflows/backend-release.yml
vendored
2
.github/workflows/backend-release.yml
vendored
|
@ -2,7 +2,7 @@ name: Upload the tagged release backend image to GHCR and Docker Hub
|
|||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: "adventurelog-backend"
|
||||
|
|
2
.github/workflows/frontend-release.yml
vendored
2
.github/workflows/frontend-release.yml
vendored
|
@ -2,7 +2,7 @@ name: Upload tagged release frontend image to GHCR and Docker Hub
|
|||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
types: [released]
|
||||
|
||||
env:
|
||||
IMAGE_NAME: "adventurelog-frontend"
|
||||
|
|
|
@ -1,28 +1,20 @@
|
|||
import os
|
||||
from django.contrib import admin
|
||||
from django.utils.html import mark_safe
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit
|
||||
from worldtravel.models import Country, Region, VisitedRegion
|
||||
|
||||
|
||||
class AdventureAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'type', 'user_id', 'date', 'is_public', 'image_display')
|
||||
list_display = ('name', 'type', 'user_id', 'is_public')
|
||||
list_filter = ('type', 'user_id', 'is_public')
|
||||
|
||||
def image_display(self, obj):
|
||||
if obj.image:
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
return mark_safe(f'<img src="{public_url}/media/{obj.image.name}" width="100px" height="100px"')
|
||||
else:
|
||||
return
|
||||
|
||||
image_display.short_description = 'Image Preview'
|
||||
search_fields = ('name',)
|
||||
|
||||
|
||||
class CountryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'country_code', 'number_of_regions')
|
||||
list_filter = ('subregion',)
|
||||
search_fields = ('name', 'country_code')
|
||||
|
||||
def number_of_regions(self, obj):
|
||||
return Region.objects.filter(country=obj).count()
|
||||
|
@ -33,6 +25,7 @@ class CountryAdmin(admin.ModelAdmin):
|
|||
class RegionAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'country', 'number_of_visits')
|
||||
list_filter = ('country',)
|
||||
search_fields = ('name', 'country__name')
|
||||
# list_filter = ('country', 'number_of_visits')
|
||||
|
||||
def number_of_visits(self, obj):
|
||||
|
@ -48,6 +41,7 @@ class CustomUserAdmin(UserAdmin):
|
|||
model = CustomUser
|
||||
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
||||
readonly_fields = ('uuid',)
|
||||
search_fields = ('username', 'email')
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
|
||||
)
|
||||
|
@ -62,6 +56,21 @@ class CustomUserAdmin(UserAdmin):
|
|||
class AdventureImageAdmin(admin.ModelAdmin):
|
||||
list_display = ('user_id', 'image_display')
|
||||
|
||||
def image_display(self, obj):
|
||||
if obj.image:
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
public_url = public_url.replace("'", "")
|
||||
return mark_safe(f'<img src="{public_url}/media/{obj.image.name}" width="100px" height="100px"')
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
class VisitAdmin(admin.ModelAdmin):
|
||||
list_display = ('adventure', 'start_date', 'end_date', 'notes')
|
||||
list_filter = ('start_date', 'end_date')
|
||||
search_fields = ('notes',)
|
||||
|
||||
|
||||
def image_display(self, obj):
|
||||
if obj.image: # Ensure this field matches your model's image field
|
||||
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||
|
@ -87,6 +96,7 @@ admin.site.register(CustomUser, CustomUserAdmin)
|
|||
|
||||
admin.site.register(Adventure, AdventureAdmin)
|
||||
admin.site.register(Collection, CollectionAdmin)
|
||||
admin.site.register(Visit, VisitAdmin)
|
||||
admin.site.register(Country, CountryAdmin)
|
||||
admin.site.register(Region, RegionAdmin)
|
||||
admin.site.register(VisitedRegion)
|
||||
|
|
32
backend/server/adventures/migrations/0007_visit_model.py
Normal file
32
backend/server/adventures/migrations/0007_visit_model.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 5.0.8 on 2024-09-23 18:06
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', '0006_alter_adventure_link'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adventure',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('general', 'General 🌍'), ('Outdoor', 'Outdoor 🏞️'), ('lodging', 'Lodging 🛌'), ('dining', 'Dining 🍽️'), ('activity', 'Activity 🏄'), ('attraction', 'Attraction 🎢'), ('shopping', 'Shopping 🛍️'), ('nightlife', 'Nightlife 🌃'), ('event', 'Event 🎉'), ('transportation', 'Transportation 🚗'), ('culture', 'Culture 🎭'), ('water_sports', 'Water Sports 🚤'), ('hiking', 'Hiking 🥾'), ('wildlife', 'Wildlife 🦒'), ('historical_sites', 'Historical Sites 🏛️'), ('music_concerts', 'Music & Concerts 🎶'), ('fitness', 'Fitness 🏋️'), ('art_museums', 'Art & Museums 🎨'), ('festivals', 'Festivals 🎪'), ('spiritual_journeys', 'Spiritual Journeys 🧘\u200d♀️'), ('volunteer_work', 'Volunteer Work 🤝'), ('other', 'Other')], default='general', max_length=100),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Visit',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('end_date', models.DateField(blank=True, null=True)),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='visits', to='adventures.adventure')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 5.0.8 on 2024-09-23 18:06
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', 'migrate_visits_categories'),
|
||||
('adventures', 'migrate_images'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='adventure',
|
||||
name='date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='adventure',
|
||||
name='end_date',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='adventure',
|
||||
name='image',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.0.8 on 2024-09-30 00:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', '0008_remove_date_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adventure',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('general', 'General 🌍'), ('outdoor', 'Outdoor 🏞️'), ('lodging', 'Lodging 🛌'), ('dining', 'Dining 🍽️'), ('activity', 'Activity 🏄'), ('attraction', 'Attraction 🎢'), ('shopping', 'Shopping 🛍️'), ('nightlife', 'Nightlife 🌃'), ('event', 'Event 🎉'), ('transportation', 'Transportation 🚗'), ('culture', 'Culture 🎭'), ('water_sports', 'Water Sports 🚤'), ('hiking', 'Hiking 🥾'), ('wildlife', 'Wildlife 🦒'), ('historical_sites', 'Historical Sites 🏛️'), ('music_concerts', 'Music & Concerts 🎶'), ('fitness', 'Fitness 🏋️'), ('art_museums', 'Art & Museums 🎨'), ('festivals', 'Festivals 🎪'), ('spiritual_journeys', 'Spiritual Journeys 🧘\u200d♀️'), ('volunteer_work', 'Volunteer Work 🤝'), ('other', 'Other')], default='general', max_length=100),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,31 @@
|
|||
from django.db import migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
def move_images_to_new_model(apps, schema_editor):
|
||||
Adventure = apps.get_model('adventures', 'Adventure')
|
||||
Visit = apps.get_model('adventures', 'Visit')
|
||||
|
||||
for adventure in Adventure.objects.all():
|
||||
# if the type is visited and there is no date, set note to 'No date provided.'
|
||||
note = 'No date provided.' if adventure.type == 'visited' and not adventure.date else ''
|
||||
if adventure.date or adventure.type == 'visited':
|
||||
Visit.objects.create(
|
||||
adventure=adventure,
|
||||
start_date=adventure.date,
|
||||
end_date=adventure.end_date,
|
||||
notes=note,
|
||||
)
|
||||
if adventure.type == 'visited' or adventure.type == 'planned':
|
||||
adventure.type = 'general'
|
||||
adventure.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('adventures', '0007_visit_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(move_images_to_new_model),
|
||||
]
|
|
@ -7,10 +7,28 @@ from django.forms import ValidationError
|
|||
from django_resized import ResizedImageField
|
||||
|
||||
ADVENTURE_TYPES = [
|
||||
('visited', 'Visited'),
|
||||
('planned', 'Planned'),
|
||||
('lodging', 'Lodging'),
|
||||
('dining', 'Dining')
|
||||
('general', 'General 🌍'),
|
||||
('outdoor', 'Outdoor 🏞️'),
|
||||
('lodging', 'Lodging 🛌'),
|
||||
('dining', 'Dining 🍽️'),
|
||||
('activity', 'Activity 🏄'),
|
||||
('attraction', 'Attraction 🎢'),
|
||||
('shopping', 'Shopping 🛍️'),
|
||||
('nightlife', 'Nightlife 🌃'),
|
||||
('event', 'Event 🎉'),
|
||||
('transportation', 'Transportation 🚗'),
|
||||
('culture', 'Culture 🎭'),
|
||||
('water_sports', 'Water Sports 🚤'),
|
||||
('hiking', 'Hiking 🥾'),
|
||||
('wildlife', 'Wildlife 🦒'),
|
||||
('historical_sites', 'Historical Sites 🏛️'),
|
||||
('music_concerts', 'Music & Concerts 🎶'),
|
||||
('fitness', 'Fitness 🏋️'),
|
||||
('art_museums', 'Art & Museums 🎨'),
|
||||
('festivals', 'Festivals 🎪'),
|
||||
('spiritual_journeys', 'Spiritual Journeys 🧘♀️'),
|
||||
('volunteer_work', 'Volunteer Work 🤝'),
|
||||
('other', 'Other')
|
||||
]
|
||||
|
||||
TRANSPORTATION_TYPES = [
|
||||
|
@ -24,19 +42,33 @@ TRANSPORTATION_TYPES = [
|
|||
('other', 'Other')
|
||||
]
|
||||
|
||||
|
||||
# Assuming you have a default user ID you want to use
|
||||
default_user_id = 1 # Replace with an actual user ID
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class Visit(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||
adventure = models.ForeignKey('Adventure', on_delete=models.CASCADE, related_name='visits')
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def clean(self):
|
||||
if self.start_date > self.end_date:
|
||||
raise ValidationError('The start date must be before or equal to the end date.')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.adventure.name} - {self.start_date} to {self.end_date}"
|
||||
|
||||
class Adventure(models.Model):
|
||||
#id = models.AutoField(primary_key=True)
|
||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||
user_id = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, default=default_user_id)
|
||||
type = models.CharField(max_length=100, choices=ADVENTURE_TYPES)
|
||||
type = models.CharField(max_length=100, choices=ADVENTURE_TYPES, default='general')
|
||||
name = models.CharField(max_length=200)
|
||||
location = models.CharField(max_length=200, blank=True, null=True)
|
||||
activity_types = ArrayField(models.CharField(
|
||||
|
@ -44,9 +76,6 @@ class Adventure(models.Model):
|
|||
description = models.TextField(blank=True, null=True)
|
||||
rating = models.FloatField(blank=True, null=True)
|
||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||
image = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='images/')
|
||||
date = models.DateField(blank=True, null=True)
|
||||
end_date = models.DateField(blank=True, null=True)
|
||||
is_public = models.BooleanField(default=False)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
|
@ -54,6 +83,12 @@ class Adventure(models.Model):
|
|||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# DEPRECATED FIELDS - TO BE REMOVED IN FUTURE VERSIONS
|
||||
# Migrations performed in this version will remove these fields
|
||||
# image = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='images/')
|
||||
# date = models.DateField(blank=True, null=True)
|
||||
# end_date = models.DateField(blank=True, null=True)
|
||||
|
||||
def clean(self):
|
||||
if self.date and self.end_date and self.date > self.end_date:
|
||||
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date))
|
||||
|
|
|
@ -31,6 +31,46 @@ class AdventureSerializer(serializers.ModelSerializer):
|
|||
representation = super().to_representation(instance)
|
||||
return representation
|
||||
|
||||
def create(self, validated_data):
|
||||
visits_data = validated_data.pop('visits', [])
|
||||
adventure = Adventure.objects.create(**validated_data)
|
||||
for visit_data in visits_data:
|
||||
Visit.objects.create(adventure=adventure, **visit_data)
|
||||
return adventure
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
visits_data = validated_data.pop('visits', [])
|
||||
|
||||
# Update Adventure fields
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Get current visits
|
||||
current_visits = instance.visits.all()
|
||||
current_visit_ids = set(current_visits.values_list('id', flat=True))
|
||||
|
||||
# Update or create visits
|
||||
updated_visit_ids = set()
|
||||
for visit_data in visits_data:
|
||||
visit_id = visit_data.get('id')
|
||||
if visit_id and visit_id in current_visit_ids:
|
||||
visit = current_visits.get(id=visit_id)
|
||||
for attr, value in visit_data.items():
|
||||
setattr(visit, attr, value)
|
||||
visit.save()
|
||||
updated_visit_ids.add(visit_id)
|
||||
else:
|
||||
# If no ID is provided or ID doesn't exist, create new visit
|
||||
new_visit = Visit.objects.create(adventure=instance, **visit_data)
|
||||
updated_visit_ids.add(new_visit.id)
|
||||
|
||||
# Delete visits that are not in the updated data
|
||||
visits_to_delete = current_visit_ids - updated_visit_ids
|
||||
instance.visits.filter(id__in=visits_to_delete).delete()
|
||||
|
||||
return instance
|
||||
|
||||
class TransportationSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -5,7 +5,7 @@ from rest_framework.decorators import action
|
|||
from rest_framework import viewsets
|
||||
from django.db.models.functions import Lower
|
||||
from rest_framework.response import Response
|
||||
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage
|
||||
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage, ADVENTURE_TYPES
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from worldtravel.models import VisitedRegion, Region, Country
|
||||
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
|
||||
|
@ -104,7 +104,10 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
@action(detail=False, methods=['get'])
|
||||
def filtered(self, request):
|
||||
types = request.query_params.get('types', '').split(',')
|
||||
valid_types = ['visited', 'planned']
|
||||
# handle case where types is all
|
||||
if 'all' in types:
|
||||
types = [t[0] for t in ADVENTURE_TYPES]
|
||||
valid_types = [t[0] for t in ADVENTURE_TYPES]
|
||||
types = [t for t in types if t in valid_types]
|
||||
|
||||
if not types:
|
||||
|
@ -113,7 +116,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
queryset = Adventure.objects.none()
|
||||
|
||||
for adventure_type in types:
|
||||
if adventure_type in ['visited', 'planned']:
|
||||
if adventure_type in valid_types:
|
||||
queryset |= Adventure.objects.filter(
|
||||
type=adventure_type, user_id=request.user.id)
|
||||
|
||||
|
@ -125,23 +128,21 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
def all(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
return Response({"error": "User is not authenticated"}, status=400)
|
||||
# include_collections = request.query_params.get('include_collections', 'false')
|
||||
# if include_collections not in ['true', 'false']:
|
||||
# include_collections = 'false'
|
||||
include_collections = request.query_params.get('include_collections', 'false')
|
||||
if include_collections not in ['true', 'false']:
|
||||
include_collections = 'false'
|
||||
|
||||
# if include_collections == 'true':
|
||||
# queryset = Adventure.objects.filter(
|
||||
# Q(is_public=True) | Q(user_id=request.user.id)
|
||||
# )
|
||||
# else:
|
||||
# queryset = Adventure.objects.filter(
|
||||
# Q(is_public=True) | Q(user_id=request.user.id), collection=None
|
||||
# )
|
||||
allowed_types = ['visited', 'planned']
|
||||
if include_collections == 'true':
|
||||
queryset = Adventure.objects.filter(
|
||||
Q(is_public=True) | Q(user_id=request.user.id)
|
||||
)
|
||||
else:
|
||||
queryset = Adventure.objects.filter(
|
||||
Q(is_public=True) | Q(user_id=request.user.id), collection=None
|
||||
)
|
||||
queryset = Adventure.objects.filter(
|
||||
Q(user_id=request.user.id) & Q(type__in=allowed_types)
|
||||
Q(user_id=request.user.id)
|
||||
)
|
||||
|
||||
queryset = self.apply_sorting(queryset)
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||
<div class="jumbotron">
|
||||
<h1>AdventureLog API Server</h1>
|
||||
<p>Welcome to the server side of AdventureLog!</p>
|
||||
<p>This site is only ment for administrative users</p>
|
||||
</div>
|
||||
{% extends "base.html" %} {% block content %}
|
||||
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||
<div class="jumbotron">
|
||||
<h1>AdventureLog API Server</h1>
|
||||
<p>
|
||||
<a class="btn btn-primary btn-lg" href="/admin" role="button">Admin Site</a>
|
||||
<a class="btn btn-secondary btn-lg" href="/docs" role="button">API Docs</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -49,18 +49,21 @@
|
|||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Add an activity"
|
||||
bind:value={inputVal}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addActivity();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Add an activity"
|
||||
bind:value={inputVal}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addActivity();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button type="button" class="btn btn-neutral" on:click={addActivity}>Add</button>
|
||||
</div>
|
||||
{#if inputVal && filteredItems.length > 0}
|
||||
<ul class="absolute z-10 w-full bg-base-100 shadow-lg max-h-60 overflow-auto">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||
import DeleteWarning from './DeleteWarning.svelte';
|
||||
import ImageDisplayModal from './ImageDisplayModal.svelte';
|
||||
import { isAdventureVisited, typeToString } from '$lib';
|
||||
|
||||
export let type: string;
|
||||
export let user: User | null;
|
||||
|
@ -27,20 +28,9 @@
|
|||
let isCollectionModalOpen: boolean = false;
|
||||
let isWarningModalOpen: boolean = false;
|
||||
|
||||
let keyword: string = '';
|
||||
let image_url: string | null = null;
|
||||
export let adventure: Adventure;
|
||||
|
||||
if (adventure.type == 'visited') {
|
||||
keyword = 'Adventure';
|
||||
} else if (adventure.type == 'planned') {
|
||||
keyword = 'Adventure';
|
||||
} else if (adventure.type == 'lodging') {
|
||||
keyword = 'Lodging';
|
||||
} else if (adventure.type == 'dining') {
|
||||
keyword = 'Dining';
|
||||
}
|
||||
|
||||
let activityTypes: string[] = [];
|
||||
// makes it reactivty to changes so it updates automatically
|
||||
$: {
|
||||
|
@ -209,16 +199,8 @@
|
|||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{#if adventure.type == 'visited'}
|
||||
<div class="badge badge-primary">Visited</div>
|
||||
{:else if adventure.type == 'planned'}
|
||||
<div class="badge badge-secondary">Planned</div>
|
||||
{:else if adventure.type == 'lodging'}
|
||||
<div class="badge badge-success">Lodging</div>
|
||||
{:else if adventure.type == 'dining'}
|
||||
<div class="badge badge-accent">Dining</div>
|
||||
{/if}
|
||||
|
||||
<div class="badge badge-primary">{typeToString(adventure.type)}</div>
|
||||
<div class="badge badge-success">{isAdventureVisited(adventure) ? 'Visited' : 'Planned'}</div>
|
||||
<div class="badge badge-secondary">{adventure.is_public ? 'Public' : 'Private'}</div>
|
||||
</div>
|
||||
{#if adventure.location && adventure.location !== ''}
|
||||
|
@ -227,16 +209,13 @@
|
|||
<p class="ml-.5">{adventure.location}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if adventure.date && adventure.date !== ''}
|
||||
<div class="inline-flex items-center">
|
||||
{#if adventure.visits.length > 0}
|
||||
<!-- visited badge -->
|
||||
<div class="flex items-center">
|
||||
<Calendar class="w-5 h-5 mr-1" />
|
||||
<p>
|
||||
{new Date(adventure.date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}{adventure.end_date && adventure.end_date !== ''
|
||||
? ' - ' +
|
||||
new Date(adventure.end_date).toLocaleDateString(undefined, { timeZone: 'UTC' })
|
||||
: ''}
|
||||
<p class="ml-.5">
|
||||
{adventure.visits.length}
|
||||
{adventure.visits.length > 1 ? 'visits' : 'visit'}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -268,7 +247,7 @@
|
|||
><Launch class="w-6 h-6" />Open Details</button
|
||||
>
|
||||
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
|
||||
<FileDocumentEdit class="w-6 h-6" />Edit {keyword}
|
||||
<FileDocumentEdit class="w-6 h-6" />Edit Adventure
|
||||
</button>
|
||||
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
|
||||
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
|
||||
|
@ -280,18 +259,12 @@
|
|||
><CheckBold class="w-6 h-6" />Mark Visited</button
|
||||
>
|
||||
{/if}
|
||||
<!-- remove from adventure -->
|
||||
{#if adventure.collection && (adventure.type == 'visited' || adventure.type == 'planned') && user?.pk == adventure.user_id}
|
||||
<!-- remove from collection -->
|
||||
{#if adventure.collection && user?.pk == adventure.user_id}
|
||||
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
|
||||
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
|
||||
>
|
||||
{/if}
|
||||
<!-- change a non adventure to an adventure -->
|
||||
{#if (adventure.collection && adventure.type == 'lodging') || adventure.type == 'dining'}
|
||||
<button class="btn btn-neutral mb-2" on:click={changeType('visited')}
|
||||
><CheckBold class="w-6 h-6" />Change to Visit</button
|
||||
>
|
||||
{/if}
|
||||
{#if !adventure.collection}
|
||||
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)}
|
||||
><Plus class="w-6 h-6" />Add to Collection</button
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Adventure, OpenStreetMapPlace, Point } from '$lib/types';
|
||||
import type { Adventure, Collection, OpenStreetMapPlace, Point } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { addToast } from '$lib/toasts';
|
||||
|
@ -8,9 +8,7 @@
|
|||
|
||||
export let longitude: number | null = null;
|
||||
export let latitude: number | null = null;
|
||||
export let collection_id: string | null = null;
|
||||
|
||||
export let is_collection: boolean = false;
|
||||
export let collection: Collection | null = null;
|
||||
|
||||
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
|
||||
|
||||
|
@ -18,27 +16,21 @@
|
|||
let places: OpenStreetMapPlace[] = [];
|
||||
let images: { id: string; image: string }[] = [];
|
||||
let warningMessage: string = '';
|
||||
let constrainDates: boolean = false;
|
||||
|
||||
import Earth from '~icons/mdi/earth';
|
||||
import ActivityComplete from './ActivityComplete.svelte';
|
||||
import { appVersion } from '$lib/config';
|
||||
|
||||
export let startDate: string | null = null;
|
||||
export let endDate: string | null = null;
|
||||
import { ADVENTURE_TYPES } from '$lib';
|
||||
|
||||
let wikiError: string = '';
|
||||
|
||||
let noPlaces: boolean = false;
|
||||
|
||||
let region_name: string | null = null;
|
||||
let region_id: string | null = null;
|
||||
|
||||
let adventure: Adventure = {
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'visited',
|
||||
date: null,
|
||||
end_date: null,
|
||||
visits: [],
|
||||
link: null,
|
||||
description: null,
|
||||
activity_types: [],
|
||||
|
@ -49,7 +41,7 @@
|
|||
location: null,
|
||||
images: [],
|
||||
user_id: null,
|
||||
collection: collection_id || null
|
||||
collection: collection?.id || null
|
||||
};
|
||||
|
||||
export let adventureToEdit: Adventure | null = null;
|
||||
|
@ -57,9 +49,7 @@
|
|||
adventure = {
|
||||
id: adventureToEdit?.id || '',
|
||||
name: adventureToEdit?.name || '',
|
||||
type: adventureToEdit?.type || 'visited',
|
||||
date: adventureToEdit?.date || null,
|
||||
end_date: adventureToEdit?.end_date || null,
|
||||
type: adventureToEdit?.type || 'general',
|
||||
link: adventureToEdit?.link || null,
|
||||
description: adventureToEdit?.description || null,
|
||||
activity_types: adventureToEdit?.activity_types || [],
|
||||
|
@ -70,7 +60,8 @@
|
|||
location: adventureToEdit?.location || null,
|
||||
images: adventureToEdit?.images || [],
|
||||
user_id: adventureToEdit?.user_id || null,
|
||||
collection: adventureToEdit?.collection || collection_id || null
|
||||
collection: adventureToEdit?.collection || collection?.id || null,
|
||||
visits: adventureToEdit?.visits || []
|
||||
};
|
||||
|
||||
let markers: Point[] = [];
|
||||
|
@ -91,7 +82,6 @@
|
|||
activity_type: ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}
|
||||
|
||||
if (longitude && latitude) {
|
||||
|
@ -109,8 +99,6 @@
|
|||
function clearMap() {
|
||||
console.log('CLEAR');
|
||||
markers = [];
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
|
||||
let imageSearch: string = adventure.name || '';
|
||||
|
@ -147,13 +135,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (adventure.type != 'visited') {
|
||||
region_id = null;
|
||||
region_name = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchImage() {
|
||||
let res = await fetch(url);
|
||||
let data = await res.blob();
|
||||
|
@ -236,6 +217,39 @@
|
|||
}
|
||||
}
|
||||
|
||||
let new_start_date: string = '';
|
||||
let new_end_date: string = '';
|
||||
let new_notes: string = '';
|
||||
function addNewVisit() {
|
||||
if (new_start_date && !new_end_date) {
|
||||
new_end_date = new_start_date;
|
||||
}
|
||||
if (new_start_date > new_end_date) {
|
||||
addToast('error', 'Start date must be before end date');
|
||||
return;
|
||||
}
|
||||
if (new_start_date === '' || new_end_date === '') {
|
||||
addToast('error', 'Please enter a start and end date');
|
||||
return;
|
||||
}
|
||||
if (new_end_date && !new_start_date) {
|
||||
addToast('error', 'Please enter a start date');
|
||||
return;
|
||||
}
|
||||
adventure.visits = [
|
||||
...adventure.visits,
|
||||
{
|
||||
start_date: new_start_date,
|
||||
end_date: new_end_date,
|
||||
notes: new_notes,
|
||||
id: ''
|
||||
}
|
||||
];
|
||||
new_start_date = '';
|
||||
new_end_date = '';
|
||||
new_notes = '';
|
||||
}
|
||||
|
||||
async function reverseGeocode() {
|
||||
let res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${adventure.latitude},${adventure.longitude}&format=jsonv2`,
|
||||
|
@ -259,7 +273,6 @@
|
|||
activity_type: data[0]?.type || ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}
|
||||
}
|
||||
console.log(data);
|
||||
|
@ -297,29 +310,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
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 = [
|
||||
|
@ -331,8 +321,6 @@
|
|||
activity_type: ''
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
|
||||
console.log(markers);
|
||||
}
|
||||
|
||||
|
@ -355,32 +343,6 @@
|
|||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (adventure.end_date && !adventure.date) {
|
||||
adventure.end_date = null;
|
||||
adventure.date = null;
|
||||
}
|
||||
|
||||
console.log(adventure);
|
||||
if (adventure.id === '') {
|
||||
let res = await fetch('/api/adventures', {
|
||||
|
@ -426,136 +388,240 @@
|
|||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div class="modal-box w-11/12 max-w-2xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">
|
||||
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-2xl">
|
||||
{adventureToEdit ? 'Edit Adventure' : 'New Adventure'}
|
||||
</h3>
|
||||
{#if adventure.id === '' || isDetails}
|
||||
<div class="modal-action items-center">
|
||||
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
|
||||
<!-- Grid layout for form fields -->
|
||||
<h2 class="text-2xl font-semibold mb-2">Basic Information</h2>
|
||||
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3"> -->
|
||||
<div>
|
||||
<label for="name">Name</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={adventure.name}
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Visited</span>
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" checked />
|
||||
<div class="collapse-title text-xl font-medium">Basic Information</div>
|
||||
<div class="collapse-content">
|
||||
<div>
|
||||
<label for="name">Name</label><br />
|
||||
<input
|
||||
type="radio"
|
||||
name="radio-10"
|
||||
class="radio checked:bg-red-500"
|
||||
on:click={() => (adventure.type = 'visited')}
|
||||
checked={adventure.type == 'visited'}
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={adventure.name}
|
||||
class="input input-bordered w-full"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Planned</span>
|
||||
</div>
|
||||
<div>
|
||||
<label for="link">Category</label><br />
|
||||
<select class="select select-bordered w-full max-w-xs" bind:value={adventure.type}>
|
||||
<option disabled selected>Select Adventure Type</option>
|
||||
{#each ADVENTURE_TYPES as type}
|
||||
<option value={type.type}>{type.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rating">Rating</label><br />
|
||||
<input
|
||||
type="radio"
|
||||
name="radio-10"
|
||||
class="radio checked:bg-blue-500"
|
||||
on:click={() => (adventure.type = 'planned')}
|
||||
checked={adventure.type == 'planned'}
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
hidden
|
||||
bind:value={adventure.rating}
|
||||
id="rating"
|
||||
name="rating"
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{#if is_collection}
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Lodging</span>
|
||||
<div class="rating -ml-3 mt-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="radio-10"
|
||||
class="radio checked:bg-blue-500"
|
||||
on:click={() => (adventure.type = 'lodging')}
|
||||
checked={adventure.type == 'lodging'}
|
||||
name="rating-2"
|
||||
class="rating-hidden"
|
||||
checked={Number.isNaN(adventure.rating)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Dining</span>
|
||||
<input
|
||||
type="radio"
|
||||
name="radio-10"
|
||||
class="radio checked:bg-blue-500"
|
||||
on:click={() => (adventure.type = 'dining')}
|
||||
checked={adventure.type == 'dining'}
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 1)}
|
||||
checked={adventure.rating === 1}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 2)}
|
||||
checked={adventure.rating === 2}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 3)}
|
||||
checked={adventure.rating === 3}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 4)}
|
||||
checked={adventure.rating === 4}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 5)}
|
||||
checked={adventure.rating === 5}
|
||||
/>
|
||||
{#if adventure.rating}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error ml-2"
|
||||
on:click={() => (adventure.rating = NaN)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<div>
|
||||
<label for="link">Link</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="link"
|
||||
name="link"
|
||||
bind:value={adventure.link}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Description</label><br />
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={adventure.description}
|
||||
class="textarea textarea-bordered w-full h-32"
|
||||
></textarea>
|
||||
<div class="mt-2">
|
||||
<div
|
||||
class="tooltip tooltip-right"
|
||||
data-tip="Pulls excerpt from Wikipedia article matching the name of the adventure."
|
||||
>
|
||||
<button type="button" class="btn btn-neutral" on:click={generateDesc}
|
||||
>Generate Description</button
|
||||
>
|
||||
</div>
|
||||
<p class="text-red-500">{wikiError}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if !collection?.id}
|
||||
<div>
|
||||
<div class="form-control flex items-start mt-1">
|
||||
<label class="label cursor-pointer flex items-start space-x-2">
|
||||
<span class="label-text">Public Adventure</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="is_public"
|
||||
name="is_public"
|
||||
bind:checked={adventure.is_public}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="date">{adventure.date ? 'Start Date' : 'Date'}</label><br />
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
name="date"
|
||||
min={startDate || ''}
|
||||
max={endDate || ''}
|
||||
bind:value={adventure.date}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
{#if adventure.date}
|
||||
<div>
|
||||
<label for="end_date">End Date</label><br />
|
||||
<input
|
||||
type="date"
|
||||
id="end_date"
|
||||
name="end_date"
|
||||
min={startDate || ''}
|
||||
max={endDate || ''}
|
||||
bind:value={adventure.end_date}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<!-- link -->
|
||||
<div>
|
||||
<label for="link">Link</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="link"
|
||||
name="link"
|
||||
bind:value={adventure.link}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">Location Information</div>
|
||||
<div class="collapse-content">
|
||||
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
|
||||
<div>
|
||||
<label for="latitude">Location</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
bind:value={adventure.location}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<form on:submit={geocode} class="mt-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Seach for a location"
|
||||
class="input input-bordered w-full max-w-xs mb-2"
|
||||
id="search"
|
||||
name="search"
|
||||
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}
|
||||
<div class="mt-4 max-w-full">
|
||||
<h3 class="font-bold text-lg mb-4">Search Results</h3>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each places as place}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
|
||||
on:click={() => {
|
||||
markers = [
|
||||
{
|
||||
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
|
||||
location: place.display_name,
|
||||
name: place.name,
|
||||
activity_type: place.type
|
||||
}
|
||||
];
|
||||
}}
|
||||
>
|
||||
{place.display_name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if noPlaces}
|
||||
<p class="text-error text-lg">No results found</p>
|
||||
{/if}
|
||||
<!-- </div> -->
|
||||
<div>
|
||||
<MapLibre
|
||||
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
||||
standardControls
|
||||
>
|
||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||
where you might not have access to the top-level `MapLibre` component. In this case
|
||||
it would also work to just use on:click on the MapLibre component itself. -->
|
||||
<MapEvents on:click={addMarker} />
|
||||
|
||||
{#each markers as marker}
|
||||
<DefaultMarker lngLat={marker.lngLat} />
|
||||
{/each}
|
||||
</MapLibre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Description</label><br />
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={adventure.description}
|
||||
class="textarea textarea-bordered w-full h-32"
|
||||
></textarea>
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-neutral" on:click={generateDesc}
|
||||
>Generate Description</button
|
||||
>
|
||||
<p class="text-red-500">{wikiError}</p>
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Activity Types ({adventure.activity_types?.length || 0})
|
||||
</div>
|
||||
<div>
|
||||
<label for="activity_types">Activity Types</label><br />
|
||||
<div class="collapse-content">
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
|
@ -566,170 +632,140 @@
|
|||
/>
|
||||
<ActivityComplete bind:activities={adventure.activity_types} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="rating"
|
||||
>Rating <iconify-icon icon="mdi:star" class="text-xl -mb-1"></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
hidden
|
||||
bind:value={adventure.rating}
|
||||
id="rating"
|
||||
name="rating"
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<div class="rating -ml-3 mt-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="rating-hidden"
|
||||
checked={Number.isNaN(adventure.rating)}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 1)}
|
||||
checked={adventure.rating === 1}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 2)}
|
||||
checked={adventure.rating === 2}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 3)}
|
||||
checked={adventure.rating === 3}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 4)}
|
||||
checked={adventure.rating === 4}
|
||||
/>
|
||||
<input
|
||||
type="radio"
|
||||
name="rating-2"
|
||||
class="mask mask-star-2 bg-orange-400"
|
||||
on:click={() => (adventure.rating = 5)}
|
||||
checked={adventure.rating === 5}
|
||||
/>
|
||||
{#if adventure.rating}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error ml-2"
|
||||
on:click={() => (adventure.rating = NaN)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse collapse-plus bg-base-200 mb-4">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
Visits ({adventure.visits.length})
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<label class="label cursor-pointer flex items-start space-x-2">
|
||||
{#if adventure.collection && collection && collection.start_date && collection.end_date}
|
||||
<span class="label-text">Constrain to collection dates</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="constrain_dates"
|
||||
name="constrain_dates"
|
||||
on:change={() => (constrainDates = !constrainDates)}
|
||||
/>
|
||||
{/if}
|
||||
</label>
|
||||
<div class="flex gap-2 mb-1">
|
||||
{#if !constrainDates}
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Start Date"
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="End Date"
|
||||
bind:value={new_end_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Start Date"
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
bind:value={new_start_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="End Date"
|
||||
bind:value={new_end_date}
|
||||
min={collection?.start_date}
|
||||
max={collection?.end_date}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !collection_id}
|
||||
<div>
|
||||
<div class="mt-2">
|
||||
<div>
|
||||
<label for="is_public"
|
||||
>Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
id="is_public"
|
||||
name="is_public"
|
||||
bind:checked={adventure.is_public}
|
||||
/>
|
||||
<div class="flex gap-2 mb-1">
|
||||
<!-- textarea for notes -->
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Add notes"
|
||||
bind:value={new_notes}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addNewVisit();
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="button" class="btn btn-neutral" on:click={addNewVisit}>Add</button>
|
||||
</div>
|
||||
|
||||
{#if adventure.visits.length > 0}
|
||||
<h2 class=" font-bold text-xl mt-2">My Visits</h2>
|
||||
{#each adventure.visits as visit}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<p>
|
||||
{new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
</p>
|
||||
{#if visit.end_date && visit.end_date !== visit.start_date}
|
||||
<p>
|
||||
{new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-error"
|
||||
on:click={() => {
|
||||
adventure.visits = adventure.visits.filter((v) => v !== visit);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="whitespace-pre-wrap -mt-2 mb-2">{visit.notes}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<h2 class="text-2xl font-semibold mb-2 mt-2">Location Information</h2>
|
||||
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
|
||||
<div>
|
||||
<label for="latitude">Location</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
bind:value={adventure.location}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<form on:submit={geocode} class="mt-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Seach for a location"
|
||||
class="input input-bordered w-full max-w-xs mb-2"
|
||||
id="search"
|
||||
name="search"
|
||||
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}
|
||||
<div class="mt-4 max-w-full">
|
||||
<h3 class="font-bold text-lg mb-4">Search Results</h3>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each places as place}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
|
||||
on:click={() => {
|
||||
markers = [
|
||||
{
|
||||
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
|
||||
location: place.display_name,
|
||||
name: place.name,
|
||||
activity_type: place.type
|
||||
}
|
||||
];
|
||||
checkPointInRegion();
|
||||
}}
|
||||
>
|
||||
{place.display_name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if noPlaces}
|
||||
<p class="text-error text-lg">No results found</p>
|
||||
{/if}
|
||||
<!-- </div> -->
|
||||
<div>
|
||||
<MapLibre
|
||||
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
||||
standardControls
|
||||
>
|
||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||
where you might not have access to the top-level `MapLibre` component. In this case
|
||||
it would also work to just use on:click on the MapLibre component itself. -->
|
||||
<MapEvents on:click={addMarker} />
|
||||
|
||||
{#each markers as marker}
|
||||
<DefaultMarker lngLat={marker.lngLat} />
|
||||
{/each}
|
||||
</MapLibre>
|
||||
</div>
|
||||
{#if region_name}
|
||||
<p class="text-lg font-semibold mt-2">Region: {region_name} ({region_id})</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
{#if warningMessage != ''}
|
||||
<div role="alert" class="alert alert-warning mb-2">
|
||||
|
|
49
frontend/src/lib/components/CategoryFilterDropdown.svelte
Normal file
49
frontend/src/lib/components/CategoryFilterDropdown.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { ADVENTURE_TYPES } from '$lib';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let types_arr: string[] = [];
|
||||
export let types: string;
|
||||
|
||||
onMount(() => {
|
||||
console.log(types);
|
||||
types_arr = types.split(',');
|
||||
});
|
||||
|
||||
function clearTypes() {
|
||||
types = '';
|
||||
types_arr = [];
|
||||
}
|
||||
|
||||
function toggleSelect(type: string) {
|
||||
if (types_arr.includes(type)) {
|
||||
types_arr = types_arr.filter((t) => t !== type);
|
||||
} else {
|
||||
types_arr.push(type);
|
||||
}
|
||||
types = types_arr.join(',');
|
||||
console.log(types);
|
||||
console.log(types_arr);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="collapse collapse-plus mb-4">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl bg-base-300 font-medium">Category Filter</div>
|
||||
<div class="collapse-content bg-base-300">
|
||||
<button class="btn btn-wide btn-neutral-300" on:click={clearTypes}>Clear</button>
|
||||
{#each ADVENTURE_TYPES as type}
|
||||
<li>
|
||||
<label class="cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
value={type.label}
|
||||
on:change={() => toggleSelect(type.type)}
|
||||
checked={types.indexOf(type.type) > -1}
|
||||
/>
|
||||
<span>{type.label}</span>
|
||||
</label>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
|
@ -66,29 +66,31 @@ export function groupAdventuresByDate(
|
|||
}
|
||||
|
||||
adventures.forEach((adventure) => {
|
||||
if (adventure.date) {
|
||||
const adventureDate = new Date(adventure.date).toISOString().split('T')[0];
|
||||
if (adventure.end_date) {
|
||||
const endDate = new Date(adventure.end_date).toISOString().split('T')[0];
|
||||
adventure.visits.forEach((visit) => {
|
||||
if (visit.start_date) {
|
||||
const adventureDate = new Date(visit.start_date).toISOString().split('T')[0];
|
||||
if (visit.end_date) {
|
||||
const endDate = new Date(visit.end_date).toISOString().split('T')[0];
|
||||
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
// Loop through all days and include adventure if it falls within the range
|
||||
for (let i = 0; i < numberOfDays; i++) {
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setUTCDate(startDate.getUTCDate() + i);
|
||||
const dateString = currentDate.toISOString().split('T')[0];
|
||||
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDate && dateString <= endDate) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
// Include the current day if it falls within the adventure date range
|
||||
if (dateString >= adventureDate && dateString <= endDate) {
|
||||
if (groupedAdventures[dateString]) {
|
||||
groupedAdventures[dateString].push(adventure);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (groupedAdventures[adventureDate]) {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
groupedAdventures[adventureDate].push(adventure);
|
||||
}
|
||||
} else if (groupedAdventures[adventureDate]) {
|
||||
// If there's no end date, add adventure to the start date only
|
||||
groupedAdventures[adventureDate].push(adventure);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return groupedAdventures;
|
||||
|
@ -216,3 +218,56 @@ export function continentCodeToString(code: string) {
|
|||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
export let ADVENTURE_TYPES = [
|
||||
{ type: 'general', label: 'General 🌍' },
|
||||
{ type: 'outdoor', label: 'Outdoor 🏞️' },
|
||||
{ type: 'lodging', label: 'Lodging 🛌' },
|
||||
{ type: 'dining', label: 'Dining 🍽️' },
|
||||
{ type: 'activity', label: 'Activity 🏄' },
|
||||
{ type: 'attraction', label: 'Attraction 🎢' },
|
||||
{ type: 'shopping', label: 'Shopping 🛍️' },
|
||||
{ type: 'nightlife', label: 'Nightlife 🌃' },
|
||||
{ type: 'event', label: 'Event 🎉' },
|
||||
{ type: 'transportation', label: 'Transportation 🚗' },
|
||||
{ type: 'culture', label: 'Culture 🎭' },
|
||||
{ type: 'water_sports', label: 'Water Sports 🚤' },
|
||||
{ type: 'hiking', label: 'Hiking 🥾' },
|
||||
{ type: 'wildlife', label: 'Wildlife 🦒' },
|
||||
{ type: 'historical_sites', label: 'Historical Sites 🏛️' },
|
||||
{ type: 'music_concerts', label: 'Music & Concerts 🎶' },
|
||||
{ type: 'fitness', label: 'Fitness 🏋️' },
|
||||
{ type: 'art_museums', label: 'Art & Museums 🎨' },
|
||||
{ type: 'festivals', label: 'Festivals 🎪' },
|
||||
{ type: 'spiritual_journeys', label: 'Spiritual Journeys 🧘♀️' },
|
||||
{ type: 'volunteer_work', label: 'Volunteer Work 🤝' },
|
||||
{ type: 'other', label: 'Other' }
|
||||
];
|
||||
|
||||
export function typeToString(type: string) {
|
||||
const typeObj = ADVENTURE_TYPES.find((t) => t.type === type);
|
||||
if (typeObj) {
|
||||
return typeObj.label;
|
||||
} else {
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an adventure has been visited.
|
||||
*
|
||||
* This function determines if the `adventure.visits` array contains at least one visit
|
||||
* with a `start_date` that is before the current date.
|
||||
*
|
||||
* @param adventure - The adventure object to check.
|
||||
* @returns `true` if the adventure has been visited, otherwise `false`.
|
||||
*/
|
||||
export function isAdventureVisited(adventure: Adventure) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Check if any visit's start_date is before the current time.
|
||||
return adventure.visits.some((visit) => {
|
||||
const visitStartTime = new Date(visit.start_date).getTime();
|
||||
return visit.start_date && visitStartTime <= currentTime;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,8 +25,12 @@ export type Adventure = {
|
|||
id: string;
|
||||
image: string;
|
||||
}[];
|
||||
date?: string | null; // Assuming date is a string in 'YYYY-MM-DD' format
|
||||
end_date?: string | null; // Assuming date is a string in 'YYYY-MM-DD' format
|
||||
visits: {
|
||||
id: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
notes: string;
|
||||
}[];
|
||||
collection?: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
|
|
|
@ -16,21 +16,11 @@ export const load = (async (event) => {
|
|||
let count = 0;
|
||||
let adventures: Adventure[] = [];
|
||||
|
||||
const visited = event.url.searchParams.get('visited');
|
||||
const planned = event.url.searchParams.get('planned');
|
||||
let typeString = event.url.searchParams.get('types');
|
||||
|
||||
let typeString: string = '';
|
||||
|
||||
if (visited == 'on') {
|
||||
typeString += 'visited';
|
||||
}
|
||||
if (planned == 'on') {
|
||||
if (typeString) {
|
||||
typeString += ',';
|
||||
}
|
||||
typeString += 'planned';
|
||||
} else if (!visited && !planned) {
|
||||
typeString = 'visited,planned';
|
||||
// If no type is specified, default to 'all'
|
||||
if (!typeString) {
|
||||
typeString = 'all';
|
||||
}
|
||||
|
||||
const include_collections = event.url.searchParams.get('include_collections') || 'false';
|
||||
|
@ -46,6 +36,7 @@ export const load = (async (event) => {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!initialFetch.ok) {
|
||||
console.error('Failed to fetch visited adventures');
|
||||
return redirect(302, '/login');
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import AdventureCard from '$lib/components/AdventureCard.svelte';
|
||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import CategoryFilterDropdown from '$lib/components/CategoryFilterDropdown.svelte';
|
||||
import NotFound from '$lib/components/NotFound.svelte';
|
||||
import type { Adventure } from '$lib/types';
|
||||
|
||||
|
@ -28,6 +29,28 @@
|
|||
let totalPages = Math.ceil(count / resultsPerPage);
|
||||
let currentPage: number = 1;
|
||||
|
||||
let typeString: string = '';
|
||||
|
||||
$: {
|
||||
if (typeof window !== 'undefined') {
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.set('types', typeString);
|
||||
goto(url.toString(), { invalidateAll: true, replaceState: true });
|
||||
}
|
||||
}
|
||||
|
||||
// sets typeString if present in the URL
|
||||
$: {
|
||||
// check to make sure its running on the client side
|
||||
if (typeof window !== 'undefined') {
|
||||
let url = new URL(window.location.href);
|
||||
let types = url.searchParams.get('types');
|
||||
if (types) {
|
||||
typeString = types;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleChangePage(pageNumber: number) {
|
||||
// let query = new URLSearchParams($page.url.searchParams.toString());
|
||||
|
||||
|
@ -216,29 +239,10 @@
|
|||
<ul class="menu p-4 w-80 h-full bg-base-200 text-base-content rounded-lg">
|
||||
<!-- Sidebar content here -->
|
||||
<div class="form-control">
|
||||
<h3 class="text-center font-bold text-lg mb-4">Adventure Types</h3>
|
||||
<!-- <h3 class="text-center font-bold text-lg mb-4">Adventure Types</h3> -->
|
||||
<form method="get">
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Completed</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="visited"
|
||||
id="visited"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={currentSort.visited}
|
||||
/>
|
||||
</label>
|
||||
<label class="label cursor-pointer">
|
||||
<span class="label-text">Planned</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="planned"
|
||||
name="planned"
|
||||
class="checkbox checkbox-primary"
|
||||
checked={currentSort.planned}
|
||||
/>
|
||||
</label>
|
||||
<!-- <div class="divider"></div> -->
|
||||
<CategoryFilterDropdown bind:types={typeString} />
|
||||
<div class="divider"></div>
|
||||
<h3 class="text-center font-bold text-lg mb-4">Sort</h3>
|
||||
<p class="text-lg font-semibold mb-2">Order Direction</p>
|
||||
<div class="join">
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
import EditAdventure from '$lib/components/AdventureModal.svelte';
|
||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
||||
import { typeToString } from '$lib';
|
||||
|
||||
onMount(() => {
|
||||
if (data.props.adventure) {
|
||||
|
@ -185,7 +186,7 @@
|
|||
>{adventure.is_public ? 'Public' : 'Private'}</span
|
||||
>
|
||||
</div>
|
||||
{#if adventure.date}
|
||||
<!-- {#if adventure.date}
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -215,7 +216,7 @@
|
|||
: ''}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if} -->
|
||||
{#if adventure.location}
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
|
@ -304,13 +305,13 @@
|
|||
></div>
|
||||
<div class="grid gap-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mt-4">Trip Details</h2>
|
||||
<h2 class="text-2xl font-bold mt-4">Adventure Details</h2>
|
||||
<div class="grid gap-4 mt-4">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Trip Type</p>
|
||||
<p class="text-sm text-muted-foreground">Adventure Type</p>
|
||||
<p class="text-base font-medium">
|
||||
{adventure.type[0].toLocaleUpperCase() + adventure.type.slice(1)}
|
||||
{typeToString(adventure.type)}
|
||||
</p>
|
||||
</div>
|
||||
{#if data.props.collection}
|
||||
|
@ -322,6 +323,36 @@
|
|||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if adventure.visits.length > 0}
|
||||
<div>
|
||||
<p class="text-sm text-muted-foreground">Visits</p>
|
||||
<p class="text-base font-medium">
|
||||
{adventure.visits.length}
|
||||
{adventure.visits.length > 1 ? 'visits' : 'visit' + ':'}
|
||||
</p>
|
||||
<!-- show each visit start and end date as well as notes -->
|
||||
{#each adventure.visits as visit}
|
||||
<div class="grid gap-2">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{visit.start_date
|
||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
{visit.end_date &&
|
||||
visit.end_date !== '' &&
|
||||
visit.end_date !== visit.start_date
|
||||
? ' - ' +
|
||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground -mt-2 mb-2">{visit.notes}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if adventure.longitude && adventure.latitude}
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
|
@ -353,11 +384,11 @@
|
|||
{adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)}
|
||||
</p>
|
||||
<p>
|
||||
{adventure.date
|
||||
<!-- {adventure.date
|
||||
? new Date(adventure.date).toLocaleDateString(undefined, {
|
||||
timeZone: 'UTC'
|
||||
})
|
||||
: ''}
|
||||
: ''} -->
|
||||
</p>
|
||||
</Popup>
|
||||
</DefaultMarker>
|
||||
|
|
|
@ -185,10 +185,7 @@
|
|||
{adventureToEdit}
|
||||
on:close={() => (isAdventureModalOpen = false)}
|
||||
on:save={saveOrCreate}
|
||||
collection_id={collection.id}
|
||||
startDate={collection.start_date}
|
||||
endDate={collection.end_date}
|
||||
is_collection={true}
|
||||
{collection}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -598,11 +595,11 @@
|
|||
<p class="font-semibold text-black text-md">
|
||||
{adventure.type.charAt(0).toUpperCase() + adventure.type.slice(1)}
|
||||
</p>
|
||||
<p>
|
||||
{adventure.date
|
||||
<!-- <p>
|
||||
{adventure.
|
||||
? new Date(adventure.date).toLocaleDateString(undefined, { timeZone: 'UTC' })
|
||||
: ''}
|
||||
</p>
|
||||
</p> -->
|
||||
</Popup>
|
||||
</DefaultMarker>
|
||||
{/if}
|
||||
|
|
|
@ -8,7 +8,7 @@ export const load = (async (event) => {
|
|||
if (!event.locals.user) {
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/`, {
|
||||
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
|
||||
headers: {
|
||||
Cookie: `${event.cookies.get('auth')}`
|
||||
}
|
||||
|
@ -25,16 +25,26 @@ export const load = (async (event) => {
|
|||
console.error('Failed to fetch visited adventures');
|
||||
return redirect(302, '/login');
|
||||
} else {
|
||||
let visited = (await visitedFetch.json()) as Adventure[];
|
||||
let visited: Adventure[] = [];
|
||||
try {
|
||||
let api_result = await visitedFetch.json();
|
||||
visited = api_result as Adventure[];
|
||||
if (!Array.isArray(visited) || visited.length === 0 || !visited) {
|
||||
throw new Error('Visited adventures response is not an array');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing visited adventures:', error);
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
// make a long lat array like this { lngLat: [-20, 0], name: 'Adventure 1' },
|
||||
let markers = visited
|
||||
.filter((adventure) => adventure.latitude !== null && adventure.longitude !== null)
|
||||
.map((adventure) => {
|
||||
return {
|
||||
lngLat: [adventure.longitude, adventure.latitude] as [number, number],
|
||||
lngLat: [adventure.longitude, adventure.latitude],
|
||||
name: adventure.name,
|
||||
type: adventure.type,
|
||||
collection: adventure.collection
|
||||
visits: adventure.visits
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
// @ts-nocheck
|
||||
|
||||
import { isAdventureVisited } from '$lib';
|
||||
import AdventureModal from '$lib/components/AdventureModal.svelte';
|
||||
import {
|
||||
DefaultMarker,
|
||||
|
@ -16,12 +18,14 @@
|
|||
|
||||
let clickedName = '';
|
||||
|
||||
console.log(data);
|
||||
|
||||
let showVisited = true;
|
||||
let showPlanned = true;
|
||||
|
||||
$: filteredMarkers = markers.filter(
|
||||
(marker) =>
|
||||
(showVisited && marker.type === 'visited') || (showPlanned && marker.type === 'planned')
|
||||
(showVisited && isAdventureVisited(marker)) || (showPlanned && !isAdventureVisited(marker))
|
||||
);
|
||||
|
||||
let newMarker = [];
|
||||
|
@ -46,7 +50,8 @@
|
|||
let newMarker = {
|
||||
lngLat: [event.detail.longitude, event.detail.latitude],
|
||||
name: event.detail.name,
|
||||
type: event.detail.type
|
||||
type: event.detail.type,
|
||||
visits: event.detail.visits
|
||||
};
|
||||
markers = [...markers, newMarker];
|
||||
clearMarkers();
|
||||
|
@ -138,11 +143,11 @@
|
|||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
||||
standardControls
|
||||
>
|
||||
{#each filteredMarkers as { lngLat, name, type }}
|
||||
{#if type == 'visited'}
|
||||
{#each filteredMarkers as marker}
|
||||
{#if isAdventureVisited(marker)}
|
||||
<Marker
|
||||
{lngLat}
|
||||
on:click={() => (clickedName = name)}
|
||||
lngLat={marker.lngLat}
|
||||
on:click={() => (clickedName = marker.name)}
|
||||
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-red-300 text-black shadow-md"
|
||||
>
|
||||
<svg
|
||||
|
@ -155,16 +160,14 @@
|
|||
<circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="red" />
|
||||
</svg>
|
||||
<Popup openOn="click" offset={[0, -10]}>
|
||||
<div class="text-lg text-black font-bold">{name}</div>
|
||||
<div class="text-lg text-black font-bold">{marker.name}</div>
|
||||
<p class="font-semibold text-black text-md">Visited</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
{/if}
|
||||
|
||||
{#if type == 'planned'}
|
||||
{:else}
|
||||
<Marker
|
||||
{lngLat}
|
||||
on:click={() => (clickedName = name)}
|
||||
lngLat={marker.lngLat}
|
||||
on:click={() => (clickedName = marker.name)}
|
||||
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 bg-blue-300 text-black shadow-2xl focus:outline-2 focus:outline-black"
|
||||
>
|
||||
<svg
|
||||
|
@ -177,7 +180,7 @@
|
|||
<circle cx="12" cy="12" r="10" stroke="blue" stroke-width="2" fill="blue" />
|
||||
</svg>
|
||||
<Popup openOn="click" offset={[0, -10]}>
|
||||
<div class="text-lg text-black font-bold">{name}</div>
|
||||
<div class="text-lg text-black font-bold">{marker.name}</div>
|
||||
<p class="font-semibold text-black text-md">Planned</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue