1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-25 07:49:37 +02:00

Merge pull request #157 from seanmorley15/development

Development
This commit is contained in:
Sean Morley 2024-08-04 13:40:42 -04:00 committed by GitHub
commit c150eee1c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 738 additions and 37 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, Collection, Transportation
from .models import Adventure, Collection, Transportation, Note
from worldtravel.models import Country, Region, VisitedRegion
@ -75,6 +75,7 @@ admin.site.register(Country, CountryAdmin)
admin.site.register(Region, RegionAdmin)
admin.site.register(VisitedRegion)
admin.site.register(Transportation)
admin.site.register(Note)
admin.site.site_header = 'AdventureLog Admin'
admin.site.site_title = 'AdventureLog Admin Site'

View file

@ -0,0 +1,35 @@
# Generated by Django 5.0.7 on 2024-08-04 01:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0013_alter_adventure_type_transportation'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='transportation',
name='type',
field=models.CharField(choices=[('car', 'Car'), ('plane', 'Plane'), ('train', 'Train'), ('bus', 'Bus'), ('boat', 'Boat'), ('bike', 'Bike'), ('walking', 'Walking'), ('other', 'Other')], max_length=100),
),
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('content', models.TextField(blank=True, null=True)),
('date', models.DateTimeField(blank=True, null=True)),
('is_public', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('collection', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.collection')),
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.7 on 2024-08-04 01:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0014_alter_transportation_type_note'),
]
operations = [
migrations.AlterField(
model_name='note',
name='date',
field=models.DateField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.7 on 2024-08-04 02:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0015_alter_note_date'),
]
operations = [
migrations.AlterField(
model_name='note',
name='date',
field=models.DateTimeField(blank=True, null=True),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 5.0.7 on 2024-08-04 02:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0016_alter_note_date'),
]
operations = [
migrations.AlterField(
model_name='note',
name='date',
field=models.DateField(blank=True, null=True),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.7 on 2024-08-04 13:19
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0017_alter_note_date'),
]
operations = [
migrations.AddField(
model_name='note',
name='links',
field=django.contrib.postgres.fields.ArrayField(base_field=models.URLField(), blank=True, null=True, size=None),
),
]

View file

@ -82,8 +82,6 @@ class Collection(models.Model):
def __str__(self):
return self.name
# make a class for transportaiotn and make it linked to a collection. Make it so it can be used for different types of transportations like car, plane, train, etc.
class Transportation(models.Model):
id = models.AutoField(primary_key=True)
user_id = models.ForeignKey(
@ -111,3 +109,26 @@ class Transportation(models.Model):
def __str__(self):
return self.name
class Note(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)
content = models.TextField(blank=True, null=True)
links = ArrayField(models.URLField(), blank=True, null=True)
date = models.DateField(blank=True, null=True)
is_public = models.BooleanField(default=False)
collection = models.ForeignKey('Collection', on_delete=models.CASCADE, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def clean(self):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Notes associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
if self.user_id != self.collection.user_id:
raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user_id.username + ' Transportation owner: ' + self.user_id.username)
def __str__(self):
return self.name

View file

@ -1,5 +1,5 @@
import os
from .models import Adventure, Collection, Transportation
from .models import Adventure, Collection, Note, Transportation
from rest_framework import serializers
class AdventureSerializer(serializers.ModelSerializer):
@ -57,15 +57,45 @@ class TransportationSerializer(serializers.ModelSerializer):
validated_data['user_id'] = self.context['request'].user
return super().create(validated_data)
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = [
'id', 'user_id', 'name', 'content', 'date', 'links',
'is_public', 'collection', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
def validate(self, data):
# Check if the collection is public and the transportation is not
collection = data.get('collection')
is_public = data.get('is_public', False)
if collection and collection.is_public and not is_public:
raise serializers.ValidationError(
'Notes associated with a public collection must be public.'
)
# Check if the user owns the collection
request = self.context.get('request')
if request and collection and collection.user_id != request.user:
raise serializers.ValidationError(
'Notes must be associated with collections owned by the same user.'
)
return data
def create(self, validated_data):
# Set the user_id to the current user
validated_data['user_id'] = self.context['request'].user
return super().create(validated_data)
class CollectionSerializer(serializers.ModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
notes = NoteSerializer(many=True, read_only=True, source='note_set')
class Meta:
model = Collection
# fields are all plus the adventures field
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations']
fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes']

View file

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

View file

@ -4,9 +4,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, Collection, Transportation
from .models import Adventure, Collection, Transportation, Note
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, CollectionSerializer, TransportationSerializer
from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer
from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
@ -279,6 +279,9 @@ class CollectionViewSet(viewsets.ModelViewSet):
# do the same for transportations
Transportation.objects.filter(collection=instance).update(is_public=new_public_status)
# do the same for notes
Note.objects.filter(collection=instance).update(is_public=new_public_status)
# Log the action (optional)
action = "public" if new_public_status else "private"
print(f"Collection {instance.id} and its adventures were set to {action}")
@ -313,6 +316,10 @@ class CollectionViewSet(viewsets.ModelViewSet):
Prefetch('transportation_set', queryset=Transportation.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
))
).prefetch_related(
Prefetch('note_set', queryset=Note.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id)
))
)
return self.apply_sorting(adventures)
@ -424,7 +431,7 @@ class TransportationViewSet(viewsets.ModelViewSet):
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all adventures is not allowed."},
return Response({"detail": "Listing all transportations is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@action(detail=False, methods=['get'])
@ -450,4 +457,37 @@ class TransportationViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)
class NoteViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['is_public', 'collection']
# return error message if user is not authenticated on the root endpoint
def list(self, request, *args, **kwargs):
# Prevent listing all adventures
return Response({"detail": "Listing all notes is not allowed."},
status=status.HTTP_403_FORBIDDEN)
@action(detail=False, methods=['get'])
def all(self, request):
if not request.user.is_authenticated:
return Response({"error": "User is not authenticated"}, status=400)
queryset = Note.objects.filter(
Q(user_id=request.user.id)
)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
"""
This view should return a list of all notes
for the currently authenticated user.
"""
user = self.request.user
return Note.objects.filter(user_id=user)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)

View file

@ -6,8 +6,14 @@ import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server';
export const authHook: Handle = async ({ event, resolve }) => {
try {
let authCookie = event.cookies.get('auth');
let refreshCookie = event.cookies.get('refresh');
if (!authCookie) {
if (!authCookie && !refreshCookie) {
event.locals.user = null;
return await resolve(event);
}
if (!authCookie && refreshCookie) {
event.locals.user = null;
const token = await tryRefreshToken(event.cookies.get('refresh') || '');
if (token) {

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { addToast } from '$lib/toasts';
import type { Note, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Launch from '~icons/mdi/launch';
import TrashCan from '~icons/mdi/trash-can';
export let note: Note;
export let user: User | null = null;
function editNote() {
dispatch('edit', note);
}
async function deleteNote() {
const res = await fetch(`/api/notes/${note.id}`, {
method: 'DELETE'
});
if (res.ok) {
addToast('success', 'Note deleted successfully');
dispatch('delete', note.id);
} else {
addToast('Failed to delete note', 'error');
}
}
</script>
<div
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 overflow-hidden text-base-content"
>
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{note.name}</h2>
<div class="card-actions justify-end">
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
><Launch class="w-6 h-6" />Open Details</button
> -->
<button class="btn btn-neutral mb-2" on:click={editNote}>
<Launch class="w-6 h-6" />Open
</button>
{#if note.user_id == user?.pk}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"
class="btn btn-warning"
on:click={deleteNote}><TrashCan class="w-6 h-6" />Delete</button
>
{/if}
</div>
</div>
</div>

View file

@ -0,0 +1,276 @@
<script lang="ts">
import { isValidUrl } from '$lib';
import type { Collection, Note, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
export let note: Note | null = null;
export let collection: Collection;
export let user: User | null = null;
let warning: string | null = '';
let newLink: string = '';
function addLink() {
// check to make it a valid URL
if (!isValidUrl(newLink)) {
warning = 'Invalid URL';
return;
} else {
warning = null;
}
if (newLink.trim().length > 0) {
newNote.links = [...newNote.links, newLink];
newLink = '';
}
console.log(newNote.links);
}
let newNote = {
name: note?.name || '',
content: note?.content || '',
date: note?.date || undefined || null,
links: note?.links || [],
collection: collection.id,
is_public: collection.is_public
};
let initialName: string = note?.name || '';
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
});
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
dispatch('close');
}
}
async function save() {
// handles empty date
if (newNote.date == '') {
newNote.date = null;
}
if (note && note.id) {
console.log('newNote', newNote);
const res = await fetch(`/api/notes/${note.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newNote)
});
if (res.ok) {
let data = await res.json();
if (data) {
dispatch('save', data);
}
} else {
console.error('Failed to save note');
}
} else {
console.log('newNote', newNote);
const res = await fetch(`/api/notes/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newNote)
});
if (res.ok) {
let data = await res.json();
if (data) {
dispatch('create', data);
}
} else {
let data = await res.json();
console.error('Failed to save note', data);
console.error('Failed to save note');
}
}
}
</script>
<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">Note Editor</h3>
{#if initialName}
<p class="font-semibold text-md mb-2">Editing note {initialName}</p>
{/if}
{#if user?.pk == note?.user_id}
<form on:submit|preventDefault>
<div class="form-control mb-2">
<label for="name">Name</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newNote.name}
/>
</div>
<div class="form-control mb-2">
<label for="content">Date</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newNote.date}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
<div class="form-control mb-2">
<label for="content">Content</label>
<textarea
id="content"
class="textarea textarea-bordered"
bind:value={newNote.content}
rows="5"
></textarea>
</div>
<div class="form-control mb-2">
<label for="content">Links</label>
<input
type="url"
class="input input-bordered w-full mb-1"
placeholder="Add a link (e.g. https://example.com)"
bind:value={newLink}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addLink();
}
}}
/>
<button type="button" class="btn btn-sm btn-primary" on:click={addLink}>Add</button>
</div>
{#if newNote.links.length > 0}
<ul class="list-none">
{#each newNote.links as link, i}
<li class="mb-1">
<a href={link} target="_blank">{link}</a>
<button
type="button"
class="btn btn-sm btn-error"
on:click={() => {
newNote.links = newNote.links.filter((_, index) => index !== i);
}}
>
Remove
</button>
</li>
{/each}
</ul>
{/if}
{#if warning}
<div role="alert" class="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{warning}</span>
</div>
{/if}
<button class="btn btn-primary" on:click={save}>Save</button>
<button class="btn btn-neutral" on:click={close}>Close</button>
{#if collection.is_public}
<div role="alert" class="alert alert-info mt-4">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>This note is public because it is in a public collection.</span>
</div>
{/if}
</form>
{:else}
<form>
<div class="form-control mb-2">
<label for="name">Name</label>
<input
type="text"
id="name"
class="input input-bordered w-full max-w-xs"
bind:value={newNote.name}
readonly
/>
</div>
<div class="form-control mb-2">
<label for="content">Date</label>
<input
type="date"
id="date"
name="date"
min={collection.start_date || ''}
max={collection.end_date || ''}
bind:value={newNote.date}
class="input input-bordered w-full max-w-xs mt-1"
readonly
/>
</div>
<div class="form-control mb-2">
<label for="content">Content</label>
<textarea
id="content"
class="textarea textarea-bordered"
bind:value={newNote.content}
rows="5"
readonly
></textarea>
</div>
<div class="form-control mb-2">
<label for="content">Links</label>
</div>
{#if newNote.links.length > 0}
<ul class="list-none">
{#each newNote.links as link, i}
<li class="mb-1">
<a href={link} target="_blank">{link}</a>
</li>
{/each}
</ul>
{/if}
<button class="btn btn-neutral" on:click={close}>Close</button>
</form>
{/if}
</div>
</dialog>

View file

@ -65,3 +65,12 @@ export async function exportData() {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
return URL.createObjectURL(blob);
}
export function isValidUrl(url: string) {
try {
new URL(url);
return true;
} catch (err) {
return false;
}
}

View file

@ -67,6 +67,7 @@ export type Collection = {
start_date?: string;
end_date?: string;
transportations?: Transportation[];
notes?: Note[];
};
export type OpenStreetMapPlace = {
@ -103,3 +104,16 @@ export type Transportation = {
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
};
export type Note = {
id: number;
user_id: number;
name: string;
content: string | null;
links: string[] | null;
date: string | null; // ISO 8601 date string
is_public: boolean;
collection: number | null;
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
};

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Adventure, Collection, Transportation } from '$lib/types';
import type { Adventure, Collection, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
@ -15,18 +15,26 @@
import TransportationCard from '$lib/components/TransportationCard.svelte';
import EditTransportation from '$lib/components/EditTransportation.svelte';
import NewTransportation from '$lib/components/NewTransportation.svelte';
import NoteCard from '$lib/components/NoteCard.svelte';
import NoteModal from '$lib/components/NoteModal.svelte';
export let data: PageData;
console.log(data);
let collection: Collection;
let adventures: Adventure[] = [];
let numVisited: number = 0;
let numAdventures: number = 0;
let transportations: Transportation[] = [];
let notes: Note[] = [];
let numberOfDays: number = NaN;
$: {
numAdventures = adventures.filter((a) => a.type === 'visited' || a.type === 'planned').length;
numVisited = adventures.filter((a) => a.type === 'visited').length;
}
@ -52,6 +60,9 @@
if (collection.transportations) {
transportations = collection.transportations;
}
if (collection.notes) {
notes = collection.notes;
}
});
function deleteAdventure(event: CustomEvent<number>) {
@ -108,6 +119,28 @@
return groupedTransportations;
}
function groupNotesByDate(notes: Note[], startDate: Date): Record<string, Note[]> {
const groupedNotes: Record<string, Note[]> = {};
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedNotes[dateString] = [];
}
notes.forEach((note) => {
if (note.date) {
const noteDate = new Date(note.date).toISOString().split('T')[0];
if (groupedNotes[noteDate]) {
groupedNotes[noteDate].push(note);
}
}
});
return groupedNotes;
}
function createAdventure(event: CustomEvent<Adventure>) {
adventures = [event.detail, ...adventures];
isShowingCreateModal = false;
@ -153,6 +186,8 @@
let transportationToEdit: Transportation;
let isEditModalOpen: boolean = false;
let isTransportationEditModalOpen: boolean = false;
let isNoteModalOpen: boolean = false;
let noteToEdit: Note | null;
let newType: string;
@ -212,6 +247,29 @@
/>
{/if}
{#if isNoteModalOpen}
<NoteModal
note={noteToEdit}
user={data.user}
on:close={() => (isNoteModalOpen = false)}
{collection}
on:save={(event) => {
notes = notes.map((note) => {
if (note.id === event.detail.id) {
return event.detail;
}
return note;
});
isNoteModalOpen = false;
}}
on:close={() => (isNoteModalOpen = false)}
on:create={(event) => {
notes = [event.detail, ...notes];
isNoteModalOpen = false;
}}
/>
{/if}
{#if isShowingCreateModal}
<NewAdventure
type={newType}
@ -331,6 +389,16 @@
>
Transportation</button
>
<button
class="btn btn-primary"
on:click={() => {
isNoteModalOpen = true;
newType = '';
noteToEdit = null;
}}
>
Note</button
>
<!-- <button
class="btn btn-primary"
@ -349,8 +417,8 @@
<div class="stats shadow bg-base-300">
<div class="stat">
<div class="stat-title">Collection Stats</div>
<div class="stat-value">{numVisited}/{adventures.length} Visited</div>
{#if numVisited === adventures.length}
<div class="stat-value">{numVisited}/{numAdventures} Visited</div>
{#if numAdventures === numVisited}
<div class="stat-desc">You've completed this collection! 🎉!</div>
{:else}
<div class="stat-desc">Keep exploring!</div>
@ -359,24 +427,28 @@
</div>
</div>
{/if}
<h1 class="text-center font-bold text-4xl mt-4 mb-2">Linked Adventures</h1>
{#if adventures.length == 0}
{#if adventures.length == 0 && transportations.length == 0}
<NotFound error={undefined} />
{/if}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
type={adventure.type}
{adventure}
on:typeChange={changeType}
/>
{/each}
</div>
{#if adventures.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-2">Linked Adventures</h1>
{#if collection.transportations && collection.transportations.length > 0}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard
user={data.user}
on:edit={editAdventure}
on:delete={deleteAdventure}
type={adventure.type}
{adventure}
on:typeChange={changeType}
/>
{/each}
</div>
{/if}
{#if transportations.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">Transportation</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each transportations as transportation}
@ -395,6 +467,25 @@
</div>
{/if}
{#if notes.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">Notes</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each notes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
/>
{/each}
</div>
{/if}
{#if collection.start_date && collection.end_date}
<h1 class="text-center font-bold text-4xl mt-4">Itinerary by Date</h1>
{#if numberOfDays}
@ -418,6 +509,7 @@
transportations,
new Date(collection.start_date)
)[dateString]}
{@const dayNotes = groupNotesByDate(notes, new Date(collection.start_date))[dateString]}
<h2 class="text-center font-semibold text-2xl mb-2 mt-4">
Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })}
@ -450,17 +542,31 @@
/>
{/each}
{/if}
{#if dayAdventures.length == 0 && dayTransportations.length == 0}
<p class="text-center text-lg mt-2">
No adventures or transportaions planned for this day.
</p>
{#if dayNotes.length > 0}
{#each dayNotes as note}
<NoteCard
{note}
user={data.user || null}
on:edit={(event) => {
noteToEdit = event.detail;
isNoteModalOpen = true;
}}
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
/>
{/each}
{/if}
{#if dayAdventures.length == 0 && dayTransportations.length == 0 && dayNotes.length == 0}
<p class="text-center text-lg mt-2">Nothing planned for this day. Enjoy the journey!</p>
{/if}
</div>
{/each}
<MapLibre
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12"
class="flex items-center self-center justify-center aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 mt-4"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,

View file

@ -0,0 +1,31 @@
import { fail, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => {
const token = event.url.searchParams.get('token');
const uid = event.url.searchParams.get('uid');
console.log('token', token);
if (!token) {
return redirect(302, '/settings/forgot-password');
} else {
let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: token,
uid: uid,
new_password1: 'password',
new_password2: 'password'
})
});
let data = await response.json();
console.log('data', data);
}
return {};
}) satisfies PageServerLoad;

View file

@ -0,0 +1,5 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>