diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py
index 6d77b07..9291219 100644
--- a/backend/server/adventures/admin.py
+++ b/backend/server/adventures/admin.py
@@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
-from .models import Adventure, Trip
+from .models import Adventure, Collection
from worldtravel.models import Country, Region, VisitedRegion
@@ -65,7 +65,7 @@ admin.site.register(Adventure, AdventureAdmin)
admin.site.register(Country, CountryAdmin)
admin.site.register(Region, RegionAdmin)
admin.site.register(VisitedRegion)
-admin.site.register(Trip)
+admin.site.register(Collection)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'
diff --git a/backend/server/adventures/migrations/0007_remove_adventure_trip_alter_adventure_type_and_more.py b/backend/server/adventures/migrations/0007_remove_adventure_trip_alter_adventure_type_and_more.py
new file mode 100644
index 0000000..6210249
--- /dev/null
+++ b/backend/server/adventures/migrations/0007_remove_adventure_trip_alter_adventure_type_and_more.py
@@ -0,0 +1,42 @@
+# Generated by Django 5.0.6 on 2024-07-15 12:57
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('adventures', '0006_alter_adventure_type_alter_trip_type'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='adventure',
+ name='trip',
+ ),
+ migrations.AlterField(
+ model_name='adventure',
+ name='type',
+ field=models.CharField(choices=[('visited', 'Visited'), ('planned', 'Planned')], max_length=100),
+ ),
+ migrations.CreateModel(
+ name='Collection',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=200)),
+ ('is_public', models.BooleanField(default=False)),
+ ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='adventure',
+ name='collection',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection'),
+ ),
+ migrations.DeleteModel(
+ name='Trip',
+ ),
+ ]
diff --git a/backend/server/adventures/migrations/0008_collection_description.py b/backend/server/adventures/migrations/0008_collection_description.py
new file mode 100644
index 0000000..8decb03
--- /dev/null
+++ b/backend/server/adventures/migrations/0008_collection_description.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.6 on 2024-07-15 13:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('adventures', '0007_remove_adventure_trip_alter_adventure_type_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='collection',
+ name='description',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py
index 57a62c6..5066ad3 100644
--- a/backend/server/adventures/models.py
+++ b/backend/server/adventures/models.py
@@ -7,7 +7,6 @@ from django.forms import ValidationError
ADVENTURE_TYPES = [
('visited', 'Visited'),
('planned', 'Planned'),
- ('featured', 'Featured')
]
@@ -34,40 +33,31 @@ class Adventure(models.Model):
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)
- trip = models.ForeignKey('Trip', on_delete=models.CASCADE, blank=True, null=True)
+ collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
def clean(self):
- if self.trip:
- if self.trip.is_public and not self.is_public:
- raise ValidationError('Adventures associated with a public trip must be public. Trip: ' + self.trip.name + ' Adventure: ' + self.name)
- if self.user_id != self.trip.user_id:
- raise ValidationError('Adventures must be associated with trips owned by the same user. Trip owner: ' + self.trip.user_id.username + ' Adventure owner: ' + self.user_id.username)
- if self.type != self.trip.type:
- raise ValidationError('Adventure type must match trip type. Trip type: ' + self.trip.type + ' Adventure type: ' + self.type)
- if self.type == 'featured' and not self.is_public:
- raise ValidationError('Featured adventures must be public. Adventure: ' + self.name)
-
+ if self.collection:
+ if self.collection.is_public and not self.is_public:
+ raise ValidationError('Adventures associated with a public collection must be public. Collection: ' + self.trip.name + ' Adventure: ' + self.name)
+ if self.user_id != self.collection.user_id:
+ raise ValidationError('Adventures must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Adventure owner: ' + self.user_id.username)
def __str__(self):
return self.name
-class Trip(models.Model):
+class Collection(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user_id)
name = models.CharField(max_length=200)
- type = models.CharField(max_length=100, choices=ADVENTURE_TYPES)
- location = models.CharField(max_length=200, blank=True, null=True)
- date = models.DateField(blank=True, null=True)
+ description = models.TextField(blank=True, null=True)
is_public = models.BooleanField(default=False)
- # if connected adventures are private and trip is public, raise an error
+ # if connected adventures are private and collection is public, raise an error
def clean(self):
if self.is_public and self.pk: # Only check if the instance has a primary key
for adventure in self.adventure_set.all():
if not adventure.is_public:
- raise ValidationError('Public trips cannot be associated with private adventures. Trip: ' + self.name + ' Adventure: ' + adventure.name)
- if self.type == 'featured' and not self.is_public:
- raise ValidationError('Featured trips must be public. Trip: ' + self.name)
+ raise ValidationError('Public collections cannot be associated with private adventures. Collection: ' + self.name + ' Adventure: ' + adventure.name)
def __str__(self):
return self.name
\ No newline at end of file
diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py
index e1d7dc8..51f2bb5 100644
--- a/backend/server/adventures/serializers.py
+++ b/backend/server/adventures/serializers.py
@@ -1,5 +1,5 @@
import os
-from .models import Adventure, Trip
+from .models import Adventure, Collection
from rest_framework import serializers
class AdventureSerializer(serializers.ModelSerializer):
@@ -18,13 +18,13 @@ class AdventureSerializer(serializers.ModelSerializer):
representation['image'] = f"{public_url}/media/{instance.image.name}"
return representation
-class TripSerializer(serializers.ModelSerializer):
+class CollectionSerializer(serializers.ModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
class Meta:
- model = Trip
+ model = Collection
# fields are all plus the adventures field
- fields = ['id', 'user_id', 'name', 'type', 'location', 'date', 'is_public', 'adventures']
+ fields = ['id', 'user_id', 'name', 'is_public', 'adventures']
\ No newline at end of file
diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py
index a855f02..2eb2573 100644
--- a/backend/server/adventures/urls.py
+++ b/backend/server/adventures/urls.py
@@ -1,10 +1,10 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
-from .views import AdventureViewSet, TripViewSet, StatsViewSet, GenerateDescription
+from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription
router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
-router.register(r'trips', TripViewSet, basename='trips')
+router.register(r'collections', CollectionViewSet, basename='collections')
router.register(r'stats', StatsViewSet, basename='stats')
router.register(r'generate', GenerateDescription, basename='generate')
diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py
index 496ded2..3800098 100644
--- a/backend/server/adventures/views.py
+++ b/backend/server/adventures/views.py
@@ -3,9 +3,9 @@ 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, Trip
+from .models import Adventure, Collection
from worldtravel.models import VisitedRegion, Region, Country
-from .serializers import AdventureSerializer, TripSerializer
+from .serializers import AdventureSerializer, CollectionSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
@@ -65,7 +65,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
@action(detail=False, methods=['get'])
def filtered(self, request):
types = request.query_params.get('types', '').split(',')
- valid_types = ['visited', 'planned', 'featured']
+ valid_types = ['visited', 'planned']
types = [t for t in types if t in valid_types]
if not types:
@@ -76,10 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
for adventure_type in types:
if adventure_type in ['visited', 'planned']:
queryset |= Adventure.objects.filter(
- type=adventure_type, user_id=request.user.id, trip=None)
- elif adventure_type == 'featured':
- queryset |= Adventure.objects.filter(
- type='featured', is_public=True, trip=None)
+ type=adventure_type, user_id=request.user.id, collection=None)
queryset = self.apply_sorting(queryset)
adventures = self.paginate_and_respond(queryset, request)
@@ -89,7 +86,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
- queryset = Adventure.objects.filter(user_id=request.user.id).exclude(type='featured')
+ queryset = Adventure.objects.filter(user_id=request.user.id)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@@ -101,39 +98,77 @@ class AdventureViewSet(viewsets.ModelViewSet):
return paginator.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
-class TripViewSet(viewsets.ModelViewSet):
- serializer_class = TripSerializer
+
+class CollectionViewSet(viewsets.ModelViewSet):
+ serializer_class = CollectionSerializer
permission_classes = [IsOwnerOrReadOnly, IsPublicReadOnly]
+ pagination_class = StandardResultsSetPagination
+
+ def apply_sorting(self, queryset):
+ order_by = self.request.query_params.get('order_by', 'name')
+ order_direction = self.request.query_params.get('order_direction', 'asc')
+
+ valid_order_by = ['name']
+ if order_by not in valid_order_by:
+ order_by = 'name'
+
+ if order_direction not in ['asc', 'desc']:
+ order_direction = 'asc'
+
+ # Apply case-insensitive sorting for the 'name' field
+ if order_by == 'name':
+ queryset = queryset.annotate(lower_name=Lower('name'))
+ ordering = 'lower_name'
+ else:
+ ordering = order_by
+
+ if order_direction == 'desc':
+ ordering = f'-{ordering}'
+
+ print(f"Ordering by: {ordering}") # For debugging
+
+ return queryset.order_by(ordering)
def get_queryset(self):
- return Trip.objects.filter(
+ collections = Collection.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
).prefetch_related(
Prefetch('adventure_set', queryset=Adventure.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
))
)
+ return self.apply_sorting(collections)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
- @action(detail=False, methods=['get'])
- @action(detail=False, methods=['get'])
- def visited(self, request):
- visited_adventures = Adventure.objects.filter(
- type='visited', user_id=request.user.id, trip=None)
- return self.get_paginated_response(visited_adventures)
+ # @action(detail=False, methods=['get'])
+ # def filtered(self, request):
+ # types = request.query_params.get('types', '').split(',')
+ # valid_types = ['visited', 'planned']
+ # types = [t for t in types if t in valid_types]
- @action(detail=False, methods=['get'])
- def planned(self, request):
- trips = self.get_queryset().filter(type='planned', user_id=request.user.id)
- serializer = self.get_serializer(trips, many=True)
- return Response(serializer.data)
+ # if not types:
+ # return Response({"error": "No valid types provided"}, status=400)
- @action(detail=False, methods=['get'])
- def featured(self, request):
- trips = self.get_queryset().filter(type='featured', is_public=True)
- serializer = self.get_serializer(trips, many=True)
+ # queryset = Collection.objects.none()
+
+ # for adventure_type in types:
+ # if adventure_type in ['visited', 'planned']:
+ # queryset |= Collection.objects.filter(
+ # type=adventure_type, user_id=request.user.id)
+
+ # queryset = self.apply_sorting(queryset)
+ # collections = self.paginate_and_respond(queryset, request)
+ # return collections
+
+ def paginate_and_respond(self, queryset, request):
+ paginator = self.pagination_class()
+ page = paginator.paginate_queryset(queryset, request)
+ if page is not None:
+ serializer = self.get_serializer(page, many=True)
+ return paginator.get_paginated_response(serializer.data)
+ serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class StatsViewSet(viewsets.ViewSet):
@@ -145,9 +180,7 @@ class StatsViewSet(viewsets.ViewSet):
type='visited', user_id=request.user.id).count()
planned_count = Adventure.objects.filter(
type='planned', user_id=request.user.id).count()
- featured_count = Adventure.objects.filter(
- type='featured', is_public=True).count()
- trips_count = Trip.objects.filter(
+ trips_count = Collection.objects.filter(
user_id=request.user.id).count()
visited_region_count = VisitedRegion.objects.filter(
user_id=request.user.id).count()
@@ -158,7 +191,6 @@ class StatsViewSet(viewsets.ViewSet):
return Response({
'visited_count': visited_count,
'planned_count': planned_count,
- 'featured_count': featured_count,
'trips_count': trips_count,
'visited_region_count': visited_region_count,
'total_regions': total_regions,
diff --git a/frontend/src/lib/components/AdventureCard.svelte b/frontend/src/lib/components/AdventureCard.svelte
index ce31967..d2685d8 100644
--- a/frontend/src/lib/components/AdventureCard.svelte
+++ b/frontend/src/lib/components/AdventureCard.svelte
@@ -100,12 +100,6 @@
>
{trip.date}
-{trip.location}
-{collection.adventures.length} Adventures
Order Direction
diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts new file mode 100644 index 0000000..1606c0b --- /dev/null +++ b/frontend/src/routes/collections/+page.server.ts @@ -0,0 +1,421 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +import type { Adventure, Collection } from '$lib/types'; + +import type { Actions, RequestEvent } from '@sveltejs/kit'; +import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; +import { checkLink } from '$lib'; + +const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load = (async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } else { + let next = null; + let previous = null; + let count = 0; + let adventures: Adventure[] = []; + let initialFetch = await fetch(`${serverEndpoint}/api/collections/`, { + headers: { + Cookie: `${event.cookies.get('auth')}` + } + }); + if (!initialFetch.ok) { + console.error('Failed to fetch visited adventures'); + return redirect(302, '/login'); + } else { + let res = await initialFetch.json(); + let visited = res.results as Adventure[]; + next = res.next; + previous = res.previous; + count = res.count; + adventures = [...adventures, ...visited]; + } + + return { + props: { + adventures, + next, + previous, + count + } + }; + } +}) satisfies PageServerLoad; + +export const actions: Actions = { + create: async (event) => { + const formData = await event.request.formData(); + + const name = formData.get('name') as string; + const description = formData.get('description') as string | null; + + if (!name) { + return { + status: 400, + body: { error: 'Missing required fields' } + }; + } + + const formDataToSend = new FormData(); + formDataToSend.append('name', name); + formDataToSend.append('description', description || ''); + let auth = event.cookies.get('auth'); + + if (!auth) { + const refresh = event.cookies.get('refresh'); + if (!refresh) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; + } + let res = await tryRefreshToken(refresh); + if (res) { + auth = res; + event.cookies.set('auth', auth, { + httpOnly: true, + sameSite: 'lax', + expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes + path: '/' + }); + } else { + return { + status: 401, + body: { message: 'Unauthorized' } + }; + } + } + + if (!auth) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; + } + + const csrfToken = await fetchCSRFToken(); + + if (!csrfToken) { + return { + status: 500, + body: { message: 'Failed to fetch CSRF token' } + }; + } + + const res = await fetch(`${serverEndpoint}/api/collections/`, { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken, + Cookie: auth + }, + body: formDataToSend + }); + + let new_id = await res.json(); + + if (!res.ok) { + const errorBody = await res.json(); + return { + status: res.status, + body: { error: errorBody } + }; + } + + let id = new_id.id; + let user_id = new_id.user_id; + + return { id, user_id }; + }, + // edit: async (event) => { + // const formData = await event.request.formData(); + + // const adventureId = formData.get('adventureId') as string; + // const type = formData.get('type') as string; + // const name = formData.get('name') as string; + // const location = formData.get('location') as string | null; + // let date = (formData.get('date') as string | null) ?? null; + // const description = formData.get('description') as string | null; + // let activity_types = formData.get('activity_types') + // ? (formData.get('activity_types') as string).split(',') + // : null; + // const rating = formData.get('rating') ? Number(formData.get('rating')) : null; + // let link = formData.get('link') as string | null; + // let latitude = formData.get('latitude') as string | null; + // let longitude = formData.get('longitude') as string | null; + // let is_public = formData.get('is_public') as string | null | boolean; + + // if (is_public) { + // is_public = true; + // } else { + // is_public = false; + // } + + // // check if latitude and longitude are valid + // if (latitude && longitude) { + // if (isNaN(Number(latitude)) || isNaN(Number(longitude))) { + // return { + // status: 400, + // body: { error: 'Invalid latitude or longitude' } + // }; + // } + // } + + // // round latitude and longitude to 6 decimal places + // if (latitude) { + // latitude = Number(latitude).toFixed(6); + // } + // if (longitude) { + // longitude = Number(longitude).toFixed(6); + // } + + // const image = formData.get('image') as File; + + // // console.log(activity_types); + + // if (!type || !name) { + // return { + // status: 400, + // body: { error: 'Missing required fields' } + // }; + // } + + // if (date == null || date == '') { + // date = null; + // } + + // if (link) { + // link = checkLink(link); + // } + + // const formDataToSend = new FormData(); + // formDataToSend.append('type', type); + // formDataToSend.append('name', name); + // formDataToSend.append('location', location || ''); + // formDataToSend.append('date', date || ''); + // formDataToSend.append('description', description || ''); + // formDataToSend.append('latitude', latitude || ''); + // formDataToSend.append('longitude', longitude || ''); + // formDataToSend.append('is_public', is_public.toString()); + // if (activity_types) { + // // Filter out empty and duplicate activity types, then trim each activity type + // const cleanedActivityTypes = Array.from( + // new Set( + // activity_types + // .map((activity_type) => activity_type.trim()) + // .filter((activity_type) => activity_type !== '' && activity_type !== ',') + // ) + // ); + + // // Append each cleaned activity type to formDataToSend + // cleanedActivityTypes.forEach((activity_type) => { + // formDataToSend.append('activity_types', activity_type); + // }); + // } + // formDataToSend.append('rating', rating ? rating.toString() : ''); + // formDataToSend.append('link', link || ''); + + // if (image && image.size > 0) { + // formDataToSend.append('image', image); + // } + + // let auth = event.cookies.get('auth'); + + // if (!auth) { + // const refresh = event.cookies.get('refresh'); + // if (!refresh) { + // return { + // status: 401, + // body: { message: 'Unauthorized' } + // }; + // } + // let res = await tryRefreshToken(refresh); + // if (res) { + // auth = res; + // event.cookies.set('auth', auth, { + // httpOnly: true, + // sameSite: 'lax', + // expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes + // path: '/' + // }); + // } else { + // return { + // status: 401, + // body: { message: 'Unauthorized' } + // }; + // } + // } + + // if (!auth) { + // return { + // status: 401, + // body: { message: 'Unauthorized' } + // }; + // } + + // const csrfToken = await fetchCSRFToken(); + + // if (!csrfToken) { + // return { + // status: 500, + // body: { message: 'Failed to fetch CSRF token' } + // }; + // } + + // const res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, { + // method: 'PATCH', + // headers: { + // 'X-CSRFToken': csrfToken, + // Cookie: auth + // }, + // body: formDataToSend + // }); + + // if (!res.ok) { + // const errorBody = await res.json(); + // return { + // status: res.status, + // body: { error: errorBody } + // }; + // } + + // let adventure = await res.json(); + + // let image_url = adventure.image; + // let link_url = adventure.link; + // return { image_url, link_url }; + // }, + get: async (event) => { + if (!event.locals.user) { + } + + const formData = await event.request.formData(); + + const order_direction = formData.get('order_direction') as string; + const order_by = formData.get('order_by') as string; + + console.log(order_direction, order_by); + + let adventures: Adventure[] = []; + + if (!event.locals.user) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; + } + + let next = null; + let previous = null; + let count = 0; + + let visitedFetch = await fetch( + `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`, + { + headers: { + Cookie: `${event.cookies.get('auth')}` + } + } + ); + if (!visitedFetch.ok) { + console.error('Failed to fetch visited adventures'); + return redirect(302, '/login'); + } else { + let res = await visitedFetch.json(); + let visited = res.results as Adventure[]; + next = res.next; + previous = res.previous; + count = res.count; + adventures = [...adventures, ...visited]; + console.log(next, previous, count); + } + + return { + adventures, + next, + previous, + count + }; + }, + changePage: async (event) => { + const formData = await event.request.formData(); + const next = formData.get('next') as string; + const previous = formData.get('previous') as string; + const page = formData.get('page') as string; + + if (!event.locals.user) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; + } + + if (!page) { + return { + status: 400, + body: { error: 'Missing required fields' } + }; + } + + // Start with the provided URL or default to the filtered adventures endpoint + let url: string = next || previous || '/api/collections/'; + + // Extract the path starting from '/api/adventures' + const apiIndex = url.indexOf('/api/collections'); + if (apiIndex !== -1) { + url = url.slice(apiIndex); + } else { + url = '/api/collections/'; + } + + // Replace or add the page number in the URL + if (url.includes('page=')) { + url = url.replace(/page=\d+/, `page=${page}`); + } else { + // If 'page=' is not in the URL, add it + url += url.includes('?') ? '&' : '?'; + url += `page=${page}`; + } + + const fullUrl = `${serverEndpoint}${url}`; + console.log(fullUrl); + console.log(serverEndpoint); + + try { + const response = await fetch(fullUrl, { + headers: { + 'Content-Type': 'application/json', + Cookie: `${event.cookies.get('auth')}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + let adventures = data.results as Adventure[]; + let next = data.next; + let previous = data.previous; + let count = data.count; + + return { + status: 200, + body: { + adventures, + next, + previous, + count, + page + } + }; + } catch (error) { + console.error('Error fetching data:', error); + return { + status: 500, + body: { error: 'Failed to fetch data' } + }; + } + } +}; diff --git a/frontend/src/routes/collections/+page.svelte b/frontend/src/routes/collections/+page.svelte new file mode 100644 index 0000000..c12177d --- /dev/null +++ b/frontend/src/routes/collections/+page.svelte @@ -0,0 +1,261 @@ + + +{#if isShowingCreateModal} +This search returned {count} results.
+ {#if collections.length === 0} ++ The adventure you were looking for could not be found. Please try a different adventure or + check back later. +
+
+
+ Visited on: {adventure.date} +
+ {/if} + {#if adventure.rating !== undefined && adventure.rating !== null} +{adventure.description}
+ {/if} + {#if adventure.link} + + {/if} + {#if adventure.activity_types && adventure.activity_types.length > 0} +Activities: 
+- The adventure you were looking for could not be found. Please try a different adventure or - check back later. -
-- There are no trips to display. Please try again later. -
-