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

checklists ui beta

This commit is contained in:
Sean Morley 2024-08-05 18:48:11 -04:00
parent f2888f26fe
commit d5f93c5d9d
8 changed files with 256 additions and 85 deletions

View file

@ -122,7 +122,7 @@ class ChecklistItemSerializer(serializers.ModelSerializer):
return super().create(validated_data) return super().create(validated_data)
class Checklist(serializers.ModelSerializer): class ChecklistSerializer(serializers.ModelSerializer):
items = ChecklistItemSerializer(many=True, read_only=True, source='checklistitem_set') items = ChecklistItemSerializer(many=True, read_only=True, source='checklistitem_set')
class Meta: class Meta:
model = Checklist model = Checklist
@ -159,7 +159,7 @@ class CollectionSerializer(serializers.ModelSerializer):
adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set') adventures = AdventureSerializer(many=True, read_only=True, source='adventure_set')
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
notes = NoteSerializer(many=True, read_only=True, source='note_set') notes = NoteSerializer(many=True, read_only=True, source='note_set')
checklists = Checklist(many=True, read_only=True, source='checklist_set') checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
class Meta: class Meta:
model = Collection model = Collection

View file

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

View file

@ -6,7 +6,7 @@ from django.db.models.functions import Lower
from rest_framework.response import Response from rest_framework.response import Response
from .models import Adventure, Checklist, Collection, Transportation, Note from .models import Adventure, Checklist, Collection, Transportation, Note
from worldtravel.models import VisitedRegion, Region, Country from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer from .serializers import AdventureSerializer, CollectionSerializer, NoteSerializer, TransportationSerializer, ChecklistSerializer
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.db.models import Q, Prefetch from django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
@ -497,3 +497,38 @@ class NoteViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save(user_id=self.request.user) serializer.save(user_id=self.request.user)
class ChecklistViewSet(viewsets.ModelViewSet):
queryset = Checklist.objects.all()
serializer_class = ChecklistSerializer
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 checklists 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 = Checklist.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 checklists
for the currently authenticated user.
"""
user = self.request.user
return Checklist.objects.filter(user_id=user)
def perform_create(self, serializer):
serializer.save(user_id=self.request.user)

View file

@ -0,0 +1,67 @@
<script lang="ts">
import { addToast } from '$lib/toasts';
import type { Checklist, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Launch from '~icons/mdi/launch';
import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar';
export let checklist: Checklist;
export let user: User | null = null;
function editNote() {
dispatch('edit', checklist);
}
async function deleteChecklist() {
const res = await fetch(`/api/checklists/${checklist.id}`, {
method: 'DELETE'
});
if (res.ok) {
addToast('success', 'Checklist deleted successfully');
dispatch('delete', checklist.id);
} else {
addToast('Failed to delete checklist', '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">
<div class="flex justify-between">
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
{checklist.name}
</h2>
</div>
<div class="badge badge-neutral">Checklist</div>
{#if checklist.items.length > 0}
<p>{checklist.items.length} {checklist.items.length > 1 ? 'Items' : 'Item'}</p>
{/if}
{#if checklist.date && checklist.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
<p>{new Date(checklist.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}</p>
</div>
{/if}
<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 checklist.user_id == user?.pk}
<button
id="delete_adventure"
data-umami-event="Delete Checklist"
class="btn btn-warning"
on:click={deleteChecklist}><TrashCan class="w-6 h-6" /></button
>
{/if}
</div>
</div>
</div>

View file

@ -7,6 +7,7 @@
import Launch from '~icons/mdi/launch'; import Launch from '~icons/mdi/launch';
import TrashCan from '~icons/mdi/trash-can'; import TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar';
export let note: Note; export let note: Note;
export let user: User | null = null; export let user: User | null = null;
@ -32,10 +33,21 @@
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" 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"> <div class="card-body">
<h2 class="card-title overflow-ellipsis">{note.name}</h2> <div class="flex justify-between">
<h2 class="text-2xl font-semibold -mt-2 break-words text-wrap">
{note.name}
</h2>
</div>
<div class="badge badge-neutral">Note</div>
{#if note.links && note.links.length > 0} {#if note.links && note.links.length > 0}
<p>{note.links.length} links</p> <p>{note.links.length} links</p>
{/if} {/if}
{#if note.date && note.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
<p>{new Date(note.date).toLocaleDateString('en-US', { timeZone: 'UTC' })}</p>
</div>
{/if}
<div class="card-actions justify-end"> <div class="card-actions justify-end">
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)} <!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
><Launch class="w-6 h-6" />Open Details</button ><Launch class="w-6 h-6" />Open Details</button

View file

@ -1,5 +1,5 @@
import inspirationalQuotes from './json/quotes.json'; import inspirationalQuotes from './json/quotes.json';
import type { Adventure, Collection } from './types'; import type { Adventure, Collection, Note, Transportation } from './types';
export function getRandomQuote() { export function getRandomQuote() {
const quotes = inspirationalQuotes.quotes; const quotes = inspirationalQuotes.quotes;
@ -74,3 +74,81 @@ export function isValidUrl(url: string) {
return false; return false;
} }
} }
export function groupAdventuresByDate(
adventures: Adventure[],
startDate: Date,
numberOfDays: number
): Record<string, Adventure[]> {
const groupedAdventures: Record<string, Adventure[]> = {};
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedAdventures[dateString] = [];
}
adventures.forEach((adventure) => {
if (adventure.date) {
const adventureDate = new Date(adventure.date).toISOString().split('T')[0];
if (groupedAdventures[adventureDate]) {
groupedAdventures[adventureDate].push(adventure);
}
}
});
return groupedAdventures;
}
export function groupTransportationsByDate(
transportations: Transportation[],
startDate: Date,
numberOfDays: number
): Record<string, Transportation[]> {
const groupedTransportations: Record<string, Transportation[]> = {};
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedTransportations[dateString] = [];
}
transportations.forEach((transportation) => {
if (transportation.date) {
const transportationDate = new Date(transportation.date).toISOString().split('T')[0];
if (groupedTransportations[transportationDate]) {
groupedTransportations[transportationDate].push(transportation);
}
}
});
return groupedTransportations;
}
export function groupNotesByDate(
notes: Note[],
startDate: Date,
numberOfDays: number
): 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;
}

View file

@ -69,6 +69,7 @@ export type Collection = {
end_date?: string; end_date?: string;
transportations?: Transportation[]; transportations?: Transportation[];
notes?: Note[]; notes?: Note[];
checklists?: Checklist[];
}; };
export type OpenStreetMapPlace = { export type OpenStreetMapPlace = {
@ -118,3 +119,25 @@ export type Note = {
created_at: string; // ISO 8601 date string created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string updated_at: string; // ISO 8601 date string
}; };
export type Checklist = {
id: number;
user_id: number;
name: string;
items: ChecklistItem[];
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
};
export type ChecklistItem = {
id: number;
user_id: number;
name: string;
is_checked: boolean;
checklist: number;
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
};

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Adventure, Collection, Note, Transportation } from '$lib/types'; import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -18,6 +18,9 @@
import NoteCard from '$lib/components/NoteCard.svelte'; import NoteCard from '$lib/components/NoteCard.svelte';
import NoteModal from '$lib/components/NoteModal.svelte'; import NoteModal from '$lib/components/NoteModal.svelte';
import { groupAdventuresByDate, groupNotesByDate, groupTransportationsByDate } from '$lib';
import ChecklistCard from '$lib/components/ChecklistCard.svelte';
export let data: PageData; export let data: PageData;
console.log(data); console.log(data);
@ -30,6 +33,7 @@
let transportations: Transportation[] = []; let transportations: Transportation[] = [];
let notes: Note[] = []; let notes: Note[] = [];
let checklists: Checklist[] = [];
let numberOfDays: number = NaN; let numberOfDays: number = NaN;
@ -63,84 +67,15 @@
if (collection.notes) { if (collection.notes) {
notes = collection.notes; notes = collection.notes;
} }
if (collection.checklists) {
checklists = collection.checklists;
}
}); });
function deleteAdventure(event: CustomEvent<number>) { function deleteAdventure(event: CustomEvent<number>) {
adventures = adventures.filter((a) => a.id !== event.detail); adventures = adventures.filter((a) => a.id !== event.detail);
} }
function groupAdventuresByDate(
adventures: Adventure[],
startDate: Date
): Record<string, Adventure[]> {
const groupedAdventures: Record<string, Adventure[]> = {};
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedAdventures[dateString] = [];
}
adventures.forEach((adventure) => {
if (adventure.date) {
const adventureDate = new Date(adventure.date).toISOString().split('T')[0];
if (groupedAdventures[adventureDate]) {
groupedAdventures[adventureDate].push(adventure);
}
}
});
return groupedAdventures;
}
function groupTransportationsByDate(
transportations: Transportation[],
startDate: Date
): Record<string, Transportation[]> {
const groupedTransportations: Record<string, Transportation[]> = {};
for (let i = 0; i < numberOfDays; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateString = currentDate.toISOString().split('T')[0];
groupedTransportations[dateString] = [];
}
transportations.forEach((transportation) => {
if (transportation.date) {
const transportationDate = new Date(transportation.date).toISOString().split('T')[0];
if (groupedTransportations[transportationDate]) {
groupedTransportations[transportationDate].push(transportation);
}
}
});
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>) { function createAdventure(event: CustomEvent<Adventure>) {
adventures = [event.detail, ...adventures]; adventures = [event.detail, ...adventures];
isShowingCreateModal = false; isShowingCreateModal = false;
@ -489,7 +424,23 @@
</div> </div>
{/if} {/if}
{#if checklists.length > 0}
<h1 class="text-center font-bold text-4xl mt-4 mb-4">Checklists</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each checklists as checklist}
<ChecklistCard
{checklist}
user={data.user || null}
on:delete={(event) => {
checklists = checklists.filter((n) => n.id != event.detail);
}}
/>
{/each}
</div>
{/if}
{#if collection.start_date && collection.end_date} {#if collection.start_date && collection.end_date}
<div class="divider"></div>
<h1 class="text-center font-bold text-4xl mt-4">Itinerary by Date</h1> <h1 class="text-center font-bold text-4xl mt-4">Itinerary by Date</h1>
{#if numberOfDays} {#if numberOfDays}
<p class="text-center text-lg pl-16 pr-16">Duration: {numberOfDays} days</p> <p class="text-center text-lg pl-16 pr-16">Duration: {numberOfDays} days</p>
@ -499,20 +450,24 @@
collection.end_date collection.end_date
).toLocaleDateString('en-US', { timeZone: 'UTC' })} ).toLocaleDateString('en-US', { timeZone: 'UTC' })}
</p> </p>
<div class="divider"></div>
{#each Array(numberOfDays) as _, i} {#each Array(numberOfDays) as _, i}
{@const currentDate = new Date(collection.start_date)} {@const currentDate = new Date(collection.start_date)}
{@const temp = currentDate.setDate(currentDate.getDate() + i)} {@const temp = currentDate.setDate(currentDate.getDate() + i)}
{@const dateString = currentDate.toISOString().split('T')[0]} {@const dateString = currentDate.toISOString().split('T')[0]}
{@const dayAdventures = groupAdventuresByDate(adventures, new Date(collection.start_date))[ {@const dayAdventures = groupAdventuresByDate(
dateString adventures,
]} new Date(collection.start_date),
numberOfDays
)[dateString]}
{@const dayTransportations = groupTransportationsByDate( {@const dayTransportations = groupTransportationsByDate(
transportations, transportations,
new Date(collection.start_date) new Date(collection.start_date),
numberOfDays
)[dateString]} )[dateString]}
{@const dayNotes = groupNotesByDate(notes, new Date(collection.start_date))[dateString]} {@const dayNotes = groupNotesByDate(notes, new Date(collection.start_date), numberOfDays)[
dateString
]}
<h2 class="text-center font-semibold text-2xl mb-2 mt-4"> <h2 class="text-center font-semibold text-2xl mb-2 mt-4">
Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })} Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })}