1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 12:59:36 +02:00

Merge pull request #240 from seanmorley15/development

Multiple images and new creation modal
This commit is contained in:
Sean Morley 2024-08-18 08:52:16 -04:00 committed by GitHub
commit b8d73635f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1146 additions and 997 deletions

View file

@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage
from worldtravel.models import Country, Region, VisitedRegion
@ -57,6 +57,20 @@ class CustomUserAdmin(UserAdmin):
else:
return
class AdventureImageAdmin(admin.ModelAdmin):
list_display = ('user_id', 'image_display')
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('/')
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'
class CollectionAdmin(admin.ModelAdmin):
def adventure_count(self, obj):
return obj.adventure_set.count()
@ -78,6 +92,7 @@ admin.site.register(Transportation)
admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(AdventureImage, AdventureImageAdmin)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-08-15 23:20
import django_resized.forms
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', 'migrate_images'),
]
operations = [
migrations.AddField(
model_name='adventure',
name='image',
field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/'),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.0.8 on 2024-08-15 23:17
import django.db.models.deletion
import django_resized.forms
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AdventureImage',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('image', django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/')),
('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adventures.adventure')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.8 on 2024-08-15 23:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0001_adventure_image'),
]
operations = [
migrations.AlterField(
model_name='adventureimage',
name='adventure',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='adventures.adventure'),
),
]

View file

@ -0,0 +1,29 @@
from django.db import migrations
def move_images_to_new_model(apps, schema_editor):
Adventure = apps.get_model('adventures', 'Adventure')
AdventureImage = apps.get_model('adventures', 'AdventureImage')
for adventure in Adventure.objects.all():
if adventure.image:
AdventureImage.objects.create(
adventure=adventure,
image=adventure.image,
user_id=adventure.user_id,
)
class Migration(migrations.Migration):
dependencies = [
('adventures', '0001_initial'),
('adventures', '0002_adventureimage'),
]
operations = [
migrations.RunPython(move_images_to_new_model),
migrations.RemoveField(
model_name='Adventure',
name='image',
),
]

View file

@ -182,3 +182,13 @@ class ChecklistItem(models.Model):
def __str__(self):
return self.name
class AdventureImage(models.Model):
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)
image = ResizedImageField(force_format="WEBP", quality=75, upload_to='images/')
adventure = models.ForeignKey(Adventure, related_name='images', on_delete=models.CASCADE)
def __str__(self):
return self.image.url

View file

@ -1,13 +1,25 @@
import os
from .models import Adventure, ChecklistItem, Collection, Note, Transportation, Checklist
from .models import Adventure, AdventureImage, ChecklistItem, Collection, Note, Transportation, Checklist
from rest_framework import serializers
class AdventureSerializer(serializers.ModelSerializer):
class AdventureImageSerializer(serializers.ModelSerializer):
class Meta:
model = Adventure
fields = '__all__'
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
model = AdventureImage
fields = ['id', 'image', 'adventure']
read_only_fields = ['id']
# def to_representation(self, instance):
# representation = super().to_representation(instance)
# # Build the full URL for the image
# request = self.context.get('request')
# if request and instance.image:
# public_url = request.build_absolute_uri(instance.image.url)
# else:
# public_url = f"{os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')}/media/{instance.image.name}"
# representation['image'] = public_url
# return representation
def to_representation(self, instance):
representation = super().to_representation(instance)
@ -18,11 +30,19 @@ class AdventureSerializer(serializers.ModelSerializer):
public_url = public_url.replace("'", "")
representation['image'] = f"{public_url}/media/{instance.image.name}"
return representation
def validate_activity_types(self, value):
if value:
return [activity.lower() for activity in value]
return value
class AdventureSerializer(serializers.ModelSerializer):
images = AdventureImageSerializer(many=True, read_only=True)
class Meta:
model = Adventure
fields = ['id', 'user_id', 'name', 'description', 'rating', 'activity_types', 'location', 'date', 'is_public', 'collection', 'created_at', 'updated_at', 'images', 'link', 'type', 'longitude', 'latitude']
read_only_fields = ['id', 'created_at', 'updated_at', 'user_id']
def to_representation(self, instance):
representation = super().to_representation(instance)
return representation
class TransportationSerializer(serializers.ModelSerializer):

View file

@ -1,6 +1,6 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet
from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet
router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
@ -11,6 +11,7 @@ router.register(r'activity-types', ActivityTypesView, basename='activity-types')
router.register(r'transportations', TransportationViewSet, basename='transportations')
router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')
router.register(r'images', AdventureImageViewSet, basename='images')
urlpatterns = [

View file

@ -1,12 +1,13 @@
import uuid
import requests
from django.db import transaction
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
from .models import Adventure, Checklist, Collection, Transportation, Note, AdventureImage
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from .serializers import AdventureImageSerializer, AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
@ -528,5 +529,97 @@ class ChecklistViewSet(viewsets.ModelViewSet):
user = self.request.user
return Checklist.objects.filter(user_id=user)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = AdventureImageSerializer
permission_classes = [IsAuthenticated]
def dispatch(self, request, *args, **kwargs):
print(f"Method: {request.method}")
return super().dispatch(request, *args, **kwargs)
@action(detail=True, methods=['post'])
def image_delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
adventure_id = request.data.get('adventure')
try:
adventure = Adventure.objects.get(id=adventure_id)
except Adventure.DoesNotExist:
return Response({"error": "Adventure not found"}, status=status.HTTP_404_NOT_FOUND)
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().update(request, *args, **kwargs)
def perform_destroy(self, instance):
print("perform_destroy")
return super().perform_destroy(instance)
def destroy(self, request, *args, **kwargs):
print("destroy")
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
def partial_update(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
instance = self.get_object()
adventure = instance.adventure
if adventure.user_id != request.user:
return Response({"error": "User does not own this adventure"}, status=status.HTTP_403_FORBIDDEN)
return super().partial_update(request, *args, **kwargs)
@action(detail=False, methods=['GET'], url_path='(?P<adventure_id>[0-9a-f-]+)')
def adventure_images(self, request, adventure_id=None, *args, **kwargs):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=status.HTTP_401_UNAUTHORIZED)
try:
adventure_uuid = uuid.UUID(adventure_id)
except ValueError:
return Response({"error": "Invalid adventure ID"}, status=status.HTTP_400_BAD_REQUEST)
queryset = AdventureImage.objects.filter(
Q(adventure__id=adventure_uuid) & Q(user_id=request.user)
)
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
def get_queryset(self):
return AdventureImage.objects.filter(user_id=self.request.user)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)

View file

@ -129,6 +129,12 @@
dispatch('edit', adventure);
}
let currentSlide = 0;
function goToSlide(index: number) {
currentSlide = index;
}
function link() {
dispatch('link', adventure);
}
@ -153,10 +159,27 @@
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl text-base-content"
>
<figure>
<!-- svelte-ignore a11y-img-redundant-alt -->
{#if adventure.image && adventure.image !== ''}
<img src={adventure.image} alt="Adventure Image" class="w-full h-48 object-cover" />
{#if adventure.images && adventure.images.length > 0}
<div class="carousel w-full">
{#each adventure.images as image, i}
<div
class="carousel-item w-full"
style="display: {i === currentSlide ? 'block' : 'none'}"
>
<img src={image.image} class="w-full h-48 object-cover" alt={adventure.name} />
<div class="flex justify-center w-full py-2 gap-2">
{#each adventure.images as _, i}
<button
on:click={() => goToSlide(i)}
class="btn btn-xs {i === currentSlide ? 'btn-active' : ''}">{i + 1}</button
>
{/each}
</div>
</div>
{/each}
</div>
{:else}
<!-- svelte-ignore a11y-img-redundant-alt -->
<img
src={'https://placehold.co/300?text=No%20Image%20Found&font=roboto'}
alt="No image available"

View file

@ -0,0 +1,689 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Adventure, OpenStreetMapPlace, Point } from '$lib/types';
import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { addToast } from '$lib/toasts';
import { deserialize } from '$app/forms';
export let longitude: number | null = null;
export let latitude: number | null = null;
export let collection_id: string | null = null;
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
let markers: Point[] = [];
let query: string = '';
let places: OpenStreetMapPlace[] = [];
let images: { id: string; image: string }[] = [];
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;
let wikiError: string = '';
let noPlaces: boolean = false;
export let adventureToEdit: Adventure | null = null;
let adventure: Adventure = {
id: adventureToEdit?.id || '',
name: adventureToEdit?.name || '',
type: adventureToEdit?.type || 'visited',
date: adventureToEdit?.date || null,
link: adventureToEdit?.link || null,
description: adventureToEdit?.description || null,
activity_types: adventureToEdit?.activity_types || [],
rating: adventureToEdit?.rating || NaN,
is_public: adventureToEdit?.is_public || false,
latitude: adventureToEdit?.latitude || NaN,
longitude: adventureToEdit?.longitude || NaN,
location: adventureToEdit?.location || null,
images: adventureToEdit?.images || [],
user_id: adventureToEdit?.user_id || null,
collection: adventureToEdit?.collection || collection_id || null
};
let url: string = '';
let imageError: string = '';
let wikiImageError: string = '';
images = adventure.images || [];
if (adventure.longitude && adventure.latitude) {
markers = [
{
lngLat: { lng: adventure.longitude, lat: adventure.latitude },
location: adventure.location || '',
name: adventure.name,
activity_type: '',
lng: 0
}
];
}
if (longitude && latitude) {
adventure.latitude = latitude;
adventure.longitude = longitude;
reverseGeocode();
}
$: {
if (!adventure.rating) {
adventure.rating = NaN;
}
}
let imageSearch: string = adventure.name || '';
async function removeImage(id: string) {
let res = await fetch(`/api/images/${id}/image_delete`, {
method: 'POST'
});
if (res.status === 204) {
images = images.filter((image) => image.id !== id);
adventure.images = images;
console.log(images);
addToast('success', 'Image removed');
} else {
addToast('error', 'Failed to remove image');
}
}
let isDetails: boolean = true;
function saveAndClose() {
dispatch('save', adventure);
close();
}
$: if (markers.length > 0) {
adventure.latitude = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
adventure.longitude = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
if (!adventure.location) {
adventure.location = markers[0].location;
}
if (!adventure.name) {
adventure.name = markers[0].name;
}
}
async function fetchImage() {
let res = await fetch(url);
let data = await res.blob();
if (!data) {
imageError = 'No image found at that URL.';
return;
}
let file = new File([data], 'image.jpg', { type: 'image/jpeg' });
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
let res2 = await fetch(`/adventures?/image`, {
method: 'POST',
body: formData
});
let data2 = await res2.json();
console.log(data2);
if (data2.type === 'success') {
images = [...images, data2];
adventure.images = images;
addToast('success', 'Image uploaded');
} else {
addToast('error', 'Failed to upload image');
}
}
async function fetchWikiImage() {
let res = await fetch(`/api/generate/img/?name=${imageSearch}`);
let data = await res.json();
if (!res.ok) {
wikiImageError = 'Failed to fetch image';
return;
}
if (data.source) {
let imageUrl = data.source;
let res = await fetch(imageUrl);
let blob = await res.blob();
let file = new File([blob], `${imageSearch}.jpg`, { type: 'image/jpeg' });
let formData = new FormData();
formData.append('image', file);
formData.append('adventure', adventure.id);
let res2 = await fetch(`/adventures?/image`, {
method: 'POST',
body: formData
});
if (res2.ok) {
let newData = deserialize(await res2.text()) as { data: { id: string; image: string } };
console.log(newData);
let newImage = { id: newData.data.id, image: newData.data.image };
console.log(newImage);
images = [...images, newImage];
adventure.images = images;
addToast('success', 'Image uploaded');
} else {
addToast('error', 'Failed to upload image');
wikiImageError = 'Failed to upload image';
}
}
}
async function geocode(e: Event | null) {
if (e) {
e.preventDefault();
}
if (!query) {
alert('Please enter a location');
return;
}
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
});
console.log(res);
let data = (await res.json()) as OpenStreetMapPlace[];
places = data;
if (data.length === 0) {
noPlaces = true;
} else {
noPlaces = false;
}
}
async function reverseGeocode() {
let res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${adventure.latitude},${adventure.longitude}&format=jsonv2`,
{
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
}
);
let data = (await res.json()) as OpenStreetMapPlace[];
if (data.length > 0) {
adventure.name = data[0]?.name || '';
adventure.activity_types?.push(data[0]?.type || '');
adventure.location = data[0]?.display_name || '';
if (longitude && latitude) {
markers = [
{
lngLat: { lng: longitude, lat: latitude },
location: data[0]?.display_name || '',
name: data[0]?.name || '',
activity_type: data[0]?.type || '',
lng: 0
}
];
}
}
console.log(data);
}
let fileInput: HTMLInputElement;
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
}
async function generateDesc() {
let res = await fetch(`/api/generate/desc/?name=${adventure.name}`);
let data = await res.json();
if (data.extract?.length > 0) {
adventure.description = data.extract;
} else {
wikiError = 'No description found';
}
}
function addMarker(e: CustomEvent<any>) {
markers = [];
markers = [
...markers,
{
lngLat: e.detail.lngLat,
name: '',
location: '',
activity_type: '',
lng: 0
}
];
console.log(markers);
}
function imageSubmit() {
return async ({ result }: any) => {
if (result.type === 'success') {
if (result.data.id && result.data.image) {
adventure.images = [...adventure.images, result.data];
images = [...images, result.data];
addToast('success', 'Image uploaded');
fileInput.value = '';
console.log(adventure);
} else {
addToast('error', result.data.error || 'Failed to upload image');
}
}
};
}
async function handleSubmit(event: Event) {
event.preventDefault();
console.log(adventure);
if (adventure.id === '') {
let res = await fetch('/api/adventures', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(adventure)
});
let data = await res.json();
if (data.id) {
adventure = data as Adventure;
isDetails = false;
addToast('success', 'Adventure created');
} else {
addToast('error', 'Failed to create adventure');
}
} else {
let res = await fetch(`/api/adventures/${adventure.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(adventure)
});
let data = await res.json();
if (data.id) {
adventure = data as Adventure;
isDetails = false;
addToast('success', 'Adventure updated');
} else {
addToast('error', 'Failed to update adventure');
}
}
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<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">Edit {adventure.type} 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>
<input
type="radio"
name="radio-10"
class="radio checked:bg-red-500"
on:click={() => (adventure.type = 'visited')}
checked={adventure.type == 'visited'}
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Planned</span>
<input
type="radio"
name="radio-10"
class="radio checked:bg-blue-500"
on:click={() => (adventure.type = 'planned')}
checked={adventure.type == 'planned'}
/>
</label>
</div>
</div>
<div>
<label for="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>
<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>
</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>
<div>
<label for="activity_types">Activity Types</label><br />
<input
type="text"
id="activity_types"
name="activity_types"
hidden
bind:value={adventure.activity_types}
class="input input-bordered w-full"
/>
<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>
{/if}
</div>
<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>
</div>
</div>
</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>
</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,
lng: 0
}
];
}}
>
{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 class="mt-4">
<button type="submit" class="btn btn-primary">Save & Next</button>
<button type="button" class="btn" on:click={close}>Close</button>
</div>
{#if adventure.is_public}
<div class="bg-neutral p-4 mt-2 rounded-md shadow-sm">
<p class=" font-semibold">Share this Adventure!</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono">
{window.location.origin}/adventures/{adventure.id}
</p>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(
`${window.location.origin}/adventures/${adventure.id}`
);
}}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2"
>
Copy Link
</button>
</div>
</div>
{/if}
</div>
</form>
</div>
{:else}
<p>Upload images here</p>
<!-- <p>{adventureToEdit.id}</p> -->
<div class="mb-2">
<label for="image">Image </label><br />
<div class="flex">
<form
method="POST"
action="/adventures?/image"
use:enhance={imageSubmit}
enctype="multipart/form-data"
>
<input
type="file"
name="image"
class="file-input file-input-bordered w-full max-w-xs"
bind:this={fileInput}
accept="image/*"
id="image"
/>
<input type="hidden" name="adventure" value={adventure.id} id="adventure" />
<button class="btn btn-neutral mt-2 mb-2" type="submit">Upload Image</button>
</form>
</div>
<div class="mt-2">
<label for="url">URL</label><br />
<input
type="text"
id="url"
name="url"
bind:value={url}
class="input input-bordered w-full"
/>
<button class="btn btn-neutral mt-2" type="button" on:click={fetchImage}
>Fetch Image</button
>
</div>
<div class="mt-2">
<label for="name">Wikipedia</label><br />
<input
type="text"
id="name"
name="name"
bind:value={imageSearch}
class="input input-bordered w-full"
/>
<button class="btn btn-neutral mt-2" type="button" on:click={fetchWikiImage}
>Fetch Image</button
>
</div>
<div class="divider"></div>
{#if images.length > 0}
<h1 class="font-semibold text-xl">My Images</h1>
{:else}
<h1 class="font-semibold text-xl">No Images</h1>
{/if}
<div class="flex flex-wrap gap-2 mt-2">
{#each images as image}
<div class="relative h-32 w-32">
<button
type="button"
class="absolute top-0 left-0 btn btn-error btn-sm z-10"
on:click={() => removeImage(image.id)}
>
X
</button>
<img src={image.image} alt={image.id} class="w-full h-full object-cover" />
</div>
{/each}
</div>
</div>
<div class="mt-4">
<button type="button" class="btn btn-primary" on:click={saveAndClose}>Close</button>
</div>
{/if}
</div>
</dialog>

View file

@ -111,50 +111,51 @@
</p>{/if}
<div class="card-actions justify-end">
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-neutral">
<DotsHorizontal class="w-6 h-6" />
{#if type == 'link'}
<button class="btn btn-primary" on:click={() => dispatch('link', collection.id)}>
<Plus class="w-5 h-5 mr-1" />
</button>
{:else}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-neutral">
<DotsHorizontal class="w-6 h-6" />
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
{#if type != 'link'}
<button
class="btn btn-neutral mb-2"
on:click={() => goto(`/collections/${collection.id}`)}
><Launch class="w-5 h-5 mr-1" />Open Details</button
>
{#if !collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
<FileDocumentEdit class="w-6 h-6" />Edit Collection
</button>
{/if}
{#if collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
<ArchiveArrowUp class="w-6 h-6 mr-1" />Unarchive
</button>
{:else}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(true)}>
<ArchiveArrowDown class="w-6 h-6 mr" />Archive
</button>
{/if}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"
class="btn btn-warning"
on:click={() => (isWarningModalOpen = true)}
><TrashCan class="w-6 h-6" />Delete</button
>
{/if}
</ul>
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
{#if type != 'link'}
<button
class="btn btn-neutral mb-2"
on:click={() => goto(`/collections/${collection.id}`)}
><Launch class="w-5 h-5 mr-1" />Open Details</button
>
{#if !collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
<FileDocumentEdit class="w-6 h-6" />Edit Collection
</button>
{/if}
{#if collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
<ArchiveArrowUp class="w-6 h-6 mr-1" />Unarchive
</button>
{:else}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(true)}>
<ArchiveArrowDown class="w-6 h-6 mr" />Archive
</button>
{/if}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"
class="btn btn-warning"
on:click={() => (isWarningModalOpen = true)}
><TrashCan class="w-6 h-6" />Delete</button
>
{/if}
{#if type == 'link'}
<button class="btn btn-primary" on:click={() => dispatch('link', collection.id)}>
<Plus class="w-5 h-5 mr-1" />
</button>
{/if}
</ul>
</div>
{/if}
</div>
</div>
</div>

View file

@ -1,363 +0,0 @@
<script lang="ts">
export let adventureToEdit: Adventure;
import { createEventDispatcher } from 'svelte';
import type { Adventure } from '$lib/types';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
export let startDate: string | null = null;
export let endDate: string | null = null;
console.log(adventureToEdit.id);
let originalName = adventureToEdit.name;
let isPointModalOpen: boolean = false;
let isImageFetcherOpen: boolean = false;
let wikiError: string = '';
let fileInput: HTMLInputElement;
let image: File;
import MapMarker from '~icons/mdi/map-marker';
import Map from '~icons/mdi/map';
import Calendar from '~icons/mdi/calendar';
import Notebook from '~icons/mdi/notebook';
import ClipboardList from '~icons/mdi/clipboard-list';
import Star from '~icons/mdi/star';
import Attachment from '~icons/mdi/attachment';
import PointSelectionModal from './PointSelectionModal.svelte';
import Earth from '~icons/mdi/earth';
import Wikipedia from '~icons/mdi/wikipedia';
import ImageFetcher from './ImageFetcher.svelte';
import ActivityComplete from './ActivityComplete.svelte';
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
function submit() {}
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
close();
}
}
async function generateDesc() {
let res = await fetch(`/api/generate/desc/?name=${adventureToEdit.name}`);
let data = await res.json();
if (!res.ok) {
wikiError = 'No article found';
}
if (data.extract) {
wikiError = '';
adventureToEdit.description = data.extract;
} else {
wikiError = 'No description found';
}
}
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method,
body: formData
});
if (response.ok) {
const result = await response.json();
const data = JSON.parse(result.data);
console.log(data);
if (data) {
if (typeof adventureToEdit.activity_types === 'string') {
adventureToEdit.activity_types = (adventureToEdit.activity_types as string)
.split(',')
.map((activity_type) => activity_type.trim())
.filter((activity_type) => activity_type !== '' && activity_type !== ',');
// Remove duplicates
adventureToEdit.activity_types = Array.from(new Set(adventureToEdit.activity_types));
}
adventureToEdit.image = data[1];
adventureToEdit.link = data[2];
addToast('success', 'Adventure edited successfully!');
dispatch('saveEdit', adventureToEdit);
close();
} else {
addToast('warning', 'Error editing adventure');
console.log('Error editing adventure');
}
}
}
function handleImageFetch(event: CustomEvent) {
const file = event.detail.file;
if (file && fileInput) {
// Create a DataTransfer object and add the file
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Set the files property of the file input
fileInput.files = dataTransfer.files;
// Update the adventureToEdit object
adventureToEdit.image = file;
}
isImageFetcherOpen = false;
}
function setLongLat(event: CustomEvent<Adventure>) {
console.log(event.detail);
isPointModalOpen = false;
}
</script>
{#if isPointModalOpen}
<PointSelectionModal
bind:adventure={adventureToEdit}
on:close={() => (isPointModalOpen = false)}
on:submit={setLongLat}
query={adventureToEdit.name}
/>
{/if}
{#if isImageFetcherOpen}
<ImageFetcher
on:image={handleImageFetch}
name={adventureToEdit.name}
on:close={() => (isImageFetcherOpen = false)}
/>
{/if}
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">Edit Adventure: {originalName}</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form method="post" style="width: 100%;" on:submit={handleSubmit} action="/adventures?/edit">
<div class="mb-2">
<input
type="text"
id="adventureId"
name="adventureId"
hidden
readonly
bind:value={adventureToEdit.id}
class="input input-bordered w-full max-w-xs mt-1"
/>
<input
type="text"
id="type"
name="type"
hidden
readonly
bind:value={adventureToEdit.type}
class="input input-bordered w-full max-w-xs mt-1"
/>
<label for="name">Name</label><br />
<input
type="text"
name="name"
id="name"
bind:value={adventureToEdit.name}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="location">Location<MapMarker class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="location"
name="location"
bind:value={adventureToEdit.location}
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="mb-2 mt-2">
<button
type="button"
class="btn btn-secondary"
on:click={() => (isPointModalOpen = true)}
>
<Map class="inline-block w-6 h-6" />{adventureToEdit.latitude &&
adventureToEdit.longitude
? 'Change'
: 'Select'}
Location</button
>
</div>
</div>
<div class="mb-2">
<label for="date">Date <Calendar class="inline-block mb-1 w-6 h-6" /></label><br />
<input
type="date"
id="date"
name="date"
min={startDate || ''}
max={endDate || ''}
bind:value={adventureToEdit.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="date">Description <Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
bind:value={adventureToEdit.description}
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
<button class="btn btn-neutral ml-2" type="button" on:click={generateDesc}
><Wikipedia class="inline-block -mt-1 mb-1 w-6 h-6" />Generate Description</button
>
</div>
{#if wikiError}
<p class="text-red-500">{wikiError}</p>
{/if}
</div>
{#if adventureToEdit.type == 'visited' || adventureToEdit.type == 'planned'}
<div class="mb-2">
<label for="activityTypes"
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="activity_types"
name="activity_types"
hidden
bind:value={adventureToEdit.activity_types}
class="input input-bordered w-full max-w-xs mt-1"
/>
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
</div>
{/if}
<div class="mb-2">
<label for="image">Image </label><br />
<div class="flex">
<input
type="file"
id="image"
name="image"
bind:value={image}
bind:this={fileInput}
class="file-input file-input-bordered w-full max-w-xs mt-1"
/>
<button
class="btn btn-neutral ml-2"
type="button"
on:click={() => (isImageFetcherOpen = true)}
><Wikipedia class="inline-block -mt-1 mb-1 w-6 h-6" />Image Search</button
>
</div>
</div>
<div class="mb-2">
<label for="link">Link <Attachment class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br
/>
<input
type="url"
id="link"
name="link"
bind:value={adventureToEdit.link}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="rating">Rating <Star class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br />
<input
type="number"
min="0"
max="5"
name="rating"
id="rating"
bind:value={adventureToEdit.rating}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<input
type="text"
id="latitude"
hidden
name="latitude"
bind:value={adventureToEdit.latitude}
class="input input-bordered w-full max-w-xs mt-1"
/>
<input
type="text"
id="longitude"
hidden
name="longitude"
bind:value={adventureToEdit.longitude}
class="input input-bordered w-full max-w-xs mt-1"
/>
{#if adventureToEdit.collection === null}
<div class="mb-2">
<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={adventureToEdit.is_public}
/>
</div>
{/if}
{#if adventureToEdit.is_public}
<div class="bg-neutral p-4 rounded-md shadow-sm">
<p class=" font-semibold">Share this Adventure!</p>
<div class="flex items-center justify-between">
<p class="text-card-foreground font-mono break-words break-all">
{window.location.origin + '/adventures/' + adventureToEdit.id}
</p>
<button
type="button"
on:click={() => {
navigator.clipboard.writeText(
`${window.location.origin}/adventures/${adventureToEdit.id}`
);
}}
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2"
>
Copy Link
</button>
</div>
</div>
{/if}
<button
type="submit"
id="edit_adventure"
data-umami-event="Edit Adventure"
class="btn btn-primary mr-4 mt-4"
on:click={submit}>Edit</button
>
<!-- if there is a button in form, it will close the modal -->
<button class="btn mt-4" on:click={close}>Close</button>
</form>
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
</div>
</div>
</dialog>

View file

@ -1,436 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Adventure, OpenStreetMapPlace, Point } from '$lib/types';
import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { addToast } from '$lib/toasts';
import PointSelectionModal from './PointSelectionModal.svelte';
import ImageFetcher from './ImageFetcher.svelte';
export let type: string = 'visited';
export let longitude: number | null = null;
export let latitude: number | null = null;
export let collection_id: string | null = null;
import MapMarker from '~icons/mdi/map-marker';
import Calendar from '~icons/mdi/calendar';
import Notebook from '~icons/mdi/notebook';
import ClipboardList from '~icons/mdi/clipboard-list';
import Star from '~icons/mdi/star';
import Attachment from '~icons/mdi/attachment';
import Map from '~icons/mdi/map';
import Earth from '~icons/mdi/earth';
import Wikipedia from '~icons/mdi/wikipedia';
import ActivityComplete from './ActivityComplete.svelte';
import { appVersion } from '$lib/config';
export let startDate: string | null = null;
export let endDate: string | null = null;
let wikiError: string = '';
let newAdventure: Adventure = {
id: '',
type: type,
name: '',
location: '',
date: '',
description: '',
activity_types: [],
rating: NaN,
link: '',
image: '',
user_id: NaN,
latitude: null,
longitude: null,
is_public: false,
collection: collection_id || ''
};
if (longitude && latitude) {
newAdventure.latitude = latitude;
newAdventure.longitude = longitude;
reverseGeocode();
}
async function reverseGeocode() {
let res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${newAdventure.latitude},${newAdventure.longitude}&format=jsonv2`,
{
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
}
);
let data = (await res.json()) as OpenStreetMapPlace[];
if (data.length > 0) {
newAdventure.name = data[0]?.name || '';
newAdventure.activity_types?.push(data[0]?.type || '');
newAdventure.location = data[0]?.display_name || '';
}
console.log(data);
}
let image: File;
let fileInput: HTMLInputElement;
let isPointModalOpen: boolean = false;
let isImageFetcherOpen: boolean = false;
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
if (isImageFetcherOpen) {
isImageFetcherOpen = false;
} else if (isPointModalOpen) {
isPointModalOpen = false;
} else {
close(); // Closes the main adventure modal
}
}
}
async function generateDesc() {
let res = await fetch(`/api/generate/desc/?name=${newAdventure.name}`);
let data = await res.json();
if (!res.ok) {
wikiError = 'No article found';
}
if (data.extract) {
wikiError = '';
newAdventure.description = data.extract;
} else {
wikiError = 'No description found';
}
}
async function handleSubmit(event: Event) {
event.preventDefault();
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: form.method,
body: formData
});
if (response.ok) {
const result = await response.json();
const data = JSON.parse(result.data); // Parsing the JSON string in the data field
if (data[1] !== undefined) {
// these two lines here are wierd, because the data[1] is the id of the new adventure and data[2] is the user_id of the new adventure
console.log(data);
let id = data[1];
let user_id = data[2];
let image_url = data[3];
let link = data[4];
if (newAdventure.is_public) {
navigator.clipboard.writeText(`${window.location.origin}/adventures/${id}`);
}
newAdventure.image = image_url;
newAdventure.id = id;
newAdventure.user_id = user_id;
newAdventure.link = link;
// turn the activity_types string into an array by splitting it at the commas
if (typeof newAdventure.activity_types === 'string') {
newAdventure.activity_types = (newAdventure.activity_types as string)
.split(',')
.map((activity_type) => activity_type.trim())
.filter((activity_type) => activity_type !== '' && activity_type !== ',');
// Remove duplicates
newAdventure.activity_types = Array.from(new Set(newAdventure.activity_types));
}
console.log(newAdventure);
dispatch('create', newAdventure);
addToast('success', 'Adventure created successfully!');
close();
}
}
}
function handleImageFetch(event: CustomEvent) {
const file = event.detail.file;
if (file && fileInput) {
// Create a DataTransfer object and add the file
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Set the files property of the file input
fileInput.files = dataTransfer.files;
// Update the adventureToEdit object
newAdventure.image = file;
}
isImageFetcherOpen = false;
}
function setLongLat(event: CustomEvent<Adventure>) {
console.log(event.detail);
isPointModalOpen = false;
}
</script>
{#if isPointModalOpen}
<PointSelectionModal
query={newAdventure.name}
on:close={() => (isPointModalOpen = false)}
on:submit={setLongLat}
bind:adventure={newAdventure}
/>
{/if}
{#if isImageFetcherOpen}
<ImageFetcher
on:image={handleImageFetch}
name={newAdventure.name}
on:close={() => (isImageFetcherOpen = false)}
/>
{/if}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">New {type} Adventure</h3>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form
method="post"
style="width: 100%;"
on:submit={handleSubmit}
action="/adventures?/create"
>
<div class="join">
<input
class="join-item btn btn-neutral"
type="radio"
name="type"
id="visited"
value="visited"
aria-label="Visited"
checked={newAdventure.type === 'visited'}
on:click={() => (type = 'visited')}
/>
<input
class="join-item btn btn-neutral"
type="radio"
name="type"
id="planned"
value="planned"
aria-label="Planned"
checked={newAdventure.type === 'planned'}
on:click={() => (type = 'planned')}
/>
</div>
<input
type="text"
name="type"
id="type"
value={type}
hidden
readonly
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="mb-2">
<label for="name">Name</label><br />
<input
type="text"
id="name"
name="name"
bind:value={newAdventure.name}
class="input input-bordered w-full max-w-xs mt-1"
required
/>
</div>
<div class="mb-2">
<label for="location">Location<MapMarker class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="location"
name="location"
bind:value={newAdventure.location}
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="mb-2 mt-2">
<button
type="button"
class="btn btn-secondary"
on:click={() => (isPointModalOpen = true)}
><Map class="inline-block w-6 h-6" />{newAdventure.latitude && newAdventure.longitude
? 'Change'
: 'Select'} Location</button
>
</div>
</div>
<div class="mb-2">
<label for="date"
>Date<iconify-icon icon="mdi:calendar" class="text-lg ml-1 -mb-0.5"
></iconify-icon></label
><br />
<input
type="date"
id="date"
name="date"
min={startDate || ''}
max={endDate || ''}
bind:value={newAdventure.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="description"
>Description<iconify-icon icon="mdi:notebook" class="text-lg ml-1 -mb-0.5"
></iconify-icon></label
><br />
<div class="flex">
<input
type="text"
id="description"
name="description"
bind:value={newAdventure.description}
class="input input-bordered w-full max-w-xs mt-1 mb-2"
/>
<button class="btn btn-neutral ml-2" type="button" on:click={generateDesc}
><Wikipedia class="inline-block -mt-1 mb-1 w-6 h-6" />Generate Description</button
>
</div>
{#if wikiError}
<p class="text-red-500">{wikiError}</p>
{/if}
</div>
{#if newAdventure.type == 'visited' || newAdventure.type == 'planned'}
<div class="mb-2">
<label for="activityTypes"
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="text"
id="activity_types"
name="activity_types"
hidden
bind:value={newAdventure.activity_types}
class="input input-bordered w-full max-w-xs mt-1"
/>
<ActivityComplete bind:activities={newAdventure.activity_types} />
</div>
{/if}
<div class="mb-2">
<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"
bind:value={newAdventure.rating}
id="rating"
name="rating"
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="link"
>Link <iconify-icon icon="mdi:link" class="text-xl -mb-1"></iconify-icon></label
><br />
<input
type="text"
id="link"
name="link"
bind:value={newAdventure.link}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<label for="image">Image </label><br />
<div class="flex">
<input
type="file"
id="image"
name="image"
bind:value={image}
bind:this={fileInput}
class="file-input file-input-bordered w-full max-w-xs mt-1"
/>
<button
class="btn btn-neutral ml-2"
type="button"
on:click={() => (isImageFetcherOpen = true)}
><Wikipedia class="inline-block -mt-1 mb-1 w-6 h-6" />Image Search</button
>
</div>
</div>
<div class="mb-2">
<input
type="text"
id="latitude"
hidden
name="latitude"
bind:value={newAdventure.latitude}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="mb-2">
<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={newAdventure.is_public}
/>
</div>
{#if newAdventure.is_public}
<p>The link to this adventure will be copied to your clipboard once it is created!</p>
{/if}
<div class="mb-2">
<input
type="text"
id="longitude"
name="longitude"
hidden
bind:value={newAdventure.longitude}
class="input input-bordered w-full max-w-xs mt-1"
/>
<input
type="text"
id="collection"
name="collection"
hidden
bind:value={newAdventure.collection}
class="input input-bordered w-full max-w-xs mt-1"
/>
<button
id="new_adventure"
data-umami-event="Create new Adventure"
type="submit"
class="btn btn-primary mr-4 mt-4">Create</button
>
<button type="button" class="btn mt-4" on:click={close}>Close</button>
</div>
</form>
</div>
</div>
</dialog>

View file

@ -11,7 +11,7 @@ export type User = {
export type Adventure = {
id: string;
user_id: number;
user_id: number | null;
type: string;
name: string;
location?: string | null;
@ -19,7 +19,10 @@ export type Adventure = {
description?: string | null;
rating?: number | null;
link?: string | null;
image?: string | null;
images: {
id: string;
image: string;
}[];
date?: string | null; // Assuming date is a string in 'YYYY-MM-DD' format
collection?: string | null;
latitude: number | null;
@ -55,6 +58,8 @@ export type Point = {
lng: number;
};
name: string;
location: string;
activity_type: string;
};
export type Collection = {

View file

@ -159,7 +159,7 @@ export const actions: Actions = {
}
formDataToSend.append('rating', rating ? rating.toString() : '');
formDataToSend.append('link', link || '');
formDataToSend.append('image', image);
// formDataToSend.append('image', image);
// log each key-value pair in the FormData
for (let pair of formDataToSend.entries()) {
@ -233,6 +233,21 @@ export const actions: Actions = {
let image_url = new_id.image;
let link_url = new_id.link;
if (image && image.size > 0) {
let imageForm = new FormData();
imageForm.append('image', image);
imageForm.append('adventure', id);
let imageRes = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST',
headers: {
Cookie: `${event.cookies.get('auth')}`
},
body: imageForm
});
let data = await imageRes.json();
console.log(data);
}
return { id, user_id, image_url, link };
},
edit: async (event) => {
@ -410,5 +425,17 @@ export const actions: Actions = {
let image_url = adventure.image;
let link_url = adventure.link;
return { image_url, link_url };
},
image: async (event) => {
let formData = await event.request.formData();
let res = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST',
headers: {
Cookie: `${event.cookies.get('auth')}`
},
body: formData
});
let data = await res.json();
return data;
}
};

View file

@ -3,8 +3,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import AdventureCard from '$lib/components/AdventureCard.svelte';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import NewAdventure from '$lib/components/NewAdventure.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import type { Adventure } from '$lib/types';
@ -23,9 +22,6 @@
includeCollections: true
};
let isShowingCreateModal: boolean = false;
let newType: string = '';
let resultsPerPage: number = 25;
let count = data.props.count || 0;
@ -95,31 +91,31 @@
}
}
let adventureToEdit: Adventure;
let isEditModalOpen: boolean = false;
let adventureToEdit: Adventure | null = null;
let isAdventureModalOpen: boolean = false;
function deleteAdventure(event: CustomEvent<string>) {
adventures = adventures.filter((adventure) => adventure.id !== event.detail);
}
function createAdventure(event: CustomEvent<Adventure>) {
adventures = [event.detail, ...adventures];
isShowingCreateModal = false;
// function that save changes to an existing adventure or creates a new one if it doesn't exist
function saveOrCreate(event: CustomEvent<Adventure>) {
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
adventures = adventures.map((adventure) => {
if (adventure.id === event.detail.id) {
return event.detail;
}
return adventure;
});
} else {
adventures = [event.detail, ...adventures];
}
isAdventureModalOpen = false;
}
function editAdventure(event: CustomEvent<Adventure>) {
adventureToEdit = event.detail;
isEditModalOpen = true;
}
function saveEdit(event: CustomEvent<Adventure>) {
adventures = adventures.map((adventure) => {
if (adventure.id === event.detail.id) {
return event.detail;
}
return adventure;
});
isEditModalOpen = false;
isAdventureModalOpen = true;
}
let sidebarOpen = false;
@ -129,19 +125,11 @@
}
</script>
{#if isShowingCreateModal}
<NewAdventure
type={newType}
on:create={createAdventure}
on:close={() => (isShowingCreateModal = false)}
/>
{/if}
{#if isEditModalOpen}
<EditAdventure
{#if isAdventureModalOpen}
<AdventureModal
{adventureToEdit}
on:close={() => (isEditModalOpen = false)}
on:saveEdit={saveEdit}
on:close={() => (isAdventureModalOpen = false)}
on:save={saveOrCreate}
/>
{/if}
@ -160,21 +148,13 @@
<button
class="btn btn-primary"
on:click={() => {
isShowingCreateModal = true;
newType = 'visited';
isAdventureModalOpen = true;
adventureToEdit = null;
}}
>
Visited Adventure</button
>
<button
class="btn btn-primary"
on:click={() => {
isShowingCreateModal = true;
newType = 'planned';
}}
>
Planned Adventure</button
Adventure</button
>
<!-- <button
class="btn btn-primary"
on:click={() => (isShowingNewTrip = true)}>Trip Planner</button

View file

@ -26,11 +26,18 @@
let adventure: Adventure;
let currentSlide = 0;
function goToSlide(index: number) {
currentSlide = index;
}
let notFound: boolean = false;
let isEditModalOpen: boolean = false;
import ClipboardList from '~icons/mdi/clipboard-list';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import EditAdventure from '$lib/components/AdventureModal.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte';
onMount(() => {
if (data.props.adventure) {
@ -69,10 +76,10 @@
{/if}
{#if isEditModalOpen}
<EditAdventure
<AdventureModal
adventureToEdit={adventure}
on:close={() => (isEditModalOpen = false)}
on:saveEdit={saveEdit}
on:save={saveEdit}
/>
{/if}
@ -92,16 +99,31 @@
<main class="flex-1">
<div class="max-w-5xl mx-auto p-4 md:p-6 lg:p-8">
<div class="grid gap-8">
{#if adventure.image}
<div>
<img
src={adventure.image}
alt={adventure.name}
width="1200"
height="600"
class="w-full h-auto object-cover rounded-lg"
style="aspect-ratio: 1200 / 600; object-fit: cover;"
/>
{#if adventure.images && adventure.images.length > 0}
<div class="carousel w-full">
{#each adventure.images as image, i}
<div
class="carousel-item w-full"
style="display: {i === currentSlide ? 'block' : 'none'}"
>
<img
src={image.image}
width="1200"
height="600"
class="w-full h-auto object-cover rounded-lg"
style="aspect-ratio: 1200 / 600; object-fit: cover;"
alt={adventure.name}
/>
<div class="flex justify-center w-full py-2 gap-2">
{#each adventure.images as _, i}
<button
on:click={() => goToSlide(i)}
class="btn btn-xs {i === currentSlide ? 'btn-active' : ''}">{i + 1}</button
>
{/each}
</div>
</div>
{/each}
</div>
{/if}
<div class="grid gap-4">
@ -109,7 +131,6 @@
<div>
<h1 class="text-4xl mt-2 font-bold">{adventure.name}</h1>
</div>
<div class="flex items-center gap-1">
{#if adventure.rating !== undefined && adventure.rating !== null}
<div class="flex justify-center items-center">

View file

@ -1,14 +1,11 @@
<script lang="ts">
import { enhance, deserialize } from '$app/forms';
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import AdventureCard from '$lib/components/AdventureCard.svelte';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import EditCollection from '$lib/components/EditCollection.svelte';
import NewAdventure from '$lib/components/NewAdventure.svelte';
import NewCollection from '$lib/components/NewCollection.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import type { Adventure, Collection } from '$lib/types';
import type { Collection } from '$lib/types';
import Plus from '~icons/mdi/plus';

View file

@ -8,9 +8,7 @@
import Plus from '~icons/mdi/plus';
import AdventureCard from '$lib/components/AdventureCard.svelte';
import AdventureLink from '$lib/components/AdventureLink.svelte';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import NewAdventure from '$lib/components/NewAdventure.svelte';
import { DefaultMarker, MapLibre, Popup } from 'svelte-maplibre';
import TransportationCard from '$lib/components/TransportationCard.svelte';
import EditTransportation from '$lib/components/EditTransportation.svelte';
@ -26,6 +24,7 @@
} from '$lib';
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
import ChecklistModal from '$lib/components/ChecklistModal.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte';
export let data: PageData;
console.log(data);
@ -83,11 +82,6 @@
adventures = adventures.filter((a) => a.id !== event.detail);
}
function createAdventure(event: CustomEvent<Adventure>) {
adventures = [event.detail, ...adventures];
isShowingCreateModal = false;
}
async function addAdventure(event: CustomEvent<Adventure>) {
console.log(event.detail);
if (adventures.find((a) => a.id === event.detail.id)) {
@ -124,9 +118,9 @@
});
}
let adventureToEdit: Adventure;
let adventureToEdit: Adventure | null = null;
let transportationToEdit: Transportation;
let isEditModalOpen: boolean = false;
let isAdventureModalOpen: boolean = false;
let isTransportationEditModalOpen: boolean = false;
let isNoteModalOpen: boolean = false;
let noteToEdit: Note | null;
@ -136,7 +130,7 @@
function editAdventure(event: CustomEvent<Adventure>) {
adventureToEdit = event.detail;
isEditModalOpen = true;
isAdventureModalOpen = true;
}
function saveNewTransportation(event: CustomEvent<Transportation>) {
@ -149,14 +143,18 @@
isTransportationEditModalOpen = false;
}
function saveEdit(event: CustomEvent<Adventure>) {
adventures = adventures.map((adventure) => {
if (adventure.id === event.detail.id) {
return event.detail;
}
return adventure;
});
isEditModalOpen = false;
function saveOrCreate(event: CustomEvent<Adventure>) {
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
adventures = adventures.map((adventure) => {
if (adventure.id === event.detail.id) {
return event.detail;
}
return adventure;
});
} else {
adventures = [event.detail, ...adventures];
}
isAdventureModalOpen = false;
}
</script>
@ -180,13 +178,12 @@
/>
{/if}
{#if isEditModalOpen}
<EditAdventure
{#if isAdventureModalOpen}
<AdventureModal
{adventureToEdit}
on:close={() => (isEditModalOpen = false)}
on:saveEdit={saveEdit}
startDate={collection.start_date}
endDate={collection.end_date}
on:close={() => (isAdventureModalOpen = false)}
on:save={saveOrCreate}
collection_id={collection.id}
/>
{/if}
@ -235,17 +232,6 @@
/>
{/if}
{#if isShowingCreateModal}
<NewAdventure
type={newType}
collection_id={collection.id}
on:create={createAdventure}
on:close={() => (isShowingCreateModal = false)}
startDate={collection.start_date}
endDate={collection.end_date}
/>
{/if}
{#if isShowingTransportationModal}
<NewTransportation
on:close={() => (isShowingTransportationModal = false)}
@ -312,21 +298,13 @@
<button
class="btn btn-primary"
on:click={() => {
isShowingCreateModal = true;
newType = 'visited';
isAdventureModalOpen = true;
adventureToEdit = null;
}}
>
Visited Adventure</button
>
<button
class="btn btn-primary"
on:click={() => {
isShowingCreateModal = true;
newType = 'planned';
}}
>
Planned Adventure</button
Adventure</button
>
<button
class="btn btn-primary"
on:click={() => {

View file

@ -1,22 +1,14 @@
<script lang="ts">
import { enhance, deserialize } from '$app/forms';
import AdventureCard from '$lib/components/AdventureCard.svelte';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import EditCollection from '$lib/components/EditCollection.svelte';
import NewAdventure from '$lib/components/NewAdventure.svelte';
import NewCollection from '$lib/components/NewCollection.svelte';
import NotFound from '$lib/components/NotFound.svelte';
import type { Adventure, Collection } from '$lib/types';
import Plus from '~icons/mdi/plus';
import type { Collection } from '$lib/types';
export let data: any;
console.log(data);
let collections: Collection[] = data.props.adventures || [];
function deleteCollection(event: CustomEvent<number>) {
function deleteCollection(event: CustomEvent<string>) {
collections = collections.filter((collection) => collection.id !== event.detail);
}
</script>

View file

@ -1,7 +1,6 @@
<script>
// @ts-nocheck
import NewAdventure from '$lib/components/NewAdventure.svelte';
import AdventureModal from '$lib/components/AdventureModal.svelte';
import {
DefaultMarker,
MapEvents,
@ -142,11 +141,11 @@
</div>
{#if createModalOpen}
<NewAdventure
<AdventureModal
on:close={() => (createModalOpen = false)}
longitude={newLongitude}
on:save={createNewAdventure}
latitude={newLatitude}
on:create={createNewAdventure}
longitude={newLongitude}
/>
{/if}

View file

@ -4,9 +4,10 @@
import type { Adventure, OpenStreetMapPlace } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import EditAdventure from '$lib/components/EditAdventure.svelte';
import EditAdventure from '$lib/components/AdventureModal.svelte';
import { appVersion } from '$lib/config';
import { goto } from '$app/navigation';
import AdventureModal from '$lib/components/AdventureModal.svelte';
export let data: PageData;
@ -59,29 +60,31 @@
}
let adventureToEdit: Adventure;
let isEditModalOpen: boolean = false;
let isAdventureModalOpen: boolean = false;
function editAdventure(event: CustomEvent<Adventure>) {
adventureToEdit = event.detail;
isEditModalOpen = true;
isAdventureModalOpen = true;
}
function saveEdit(event: CustomEvent<Adventure>) {
console.log(event.detail);
myAdventures = myAdventures.map((adventure) => {
if (adventure.id === event.detail.id) {
return event.detail;
}
return adventure;
});
isEditModalOpen = false;
isAdventureModalOpen = false;
console.log(myAdventures);
}
</script>
{#if isEditModalOpen}
<EditAdventure
{#if isAdventureModalOpen}
<AdventureModal
{adventureToEdit}
on:close={() => (isEditModalOpen = false)}
on:saveEdit={saveEdit}
on:close={() => (isAdventureModalOpen = false)}
on:save={filterByProperty}
/>
{/if}