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)
class Checklist(serializers.ModelSerializer):
class ChecklistSerializer(serializers.ModelSerializer):
items = ChecklistItemSerializer(many=True, read_only=True, source='checklistitem_set')
class Meta:
model = Checklist
@ -159,7 +159,7 @@ 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')
checklists = Checklist(many=True, read_only=True, source='checklist_set')
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
class Meta:
model = Collection

View file

@ -1,6 +1,6 @@
from django.urls import include, path
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.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'transportations', TransportationViewSet, basename='transportations')
router.register(r'notes', NoteViewSet, basename='notes')
router.register(r'checklists', ChecklistViewSet, basename='checklists')
urlpatterns = [

View file

@ -6,7 +6,7 @@ from django.db.models.functions import Lower
from rest_framework.response import Response
from .models import Adventure, Checklist, Collection, Transportation, Note
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 django.db.models import Q, Prefetch
from .permissions import IsOwnerOrReadOnly, IsPublicReadOnly
@ -497,3 +497,38 @@ class NoteViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
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 TrashCan from '~icons/mdi/trash-can';
import Calendar from '~icons/mdi/calendar';
export let note: Note;
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"
>
<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}
<p>{note.links.length} links</p>
{/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">
<!-- <button class="btn btn-neutral mb-2" on:click={() => goto(`/notes/${note.id}`)}
><Launch class="w-6 h-6" />Open Details</button

View file

@ -1,5 +1,5 @@
import inspirationalQuotes from './json/quotes.json';
import type { Adventure, Collection } from './types';
import type { Adventure, Collection, Note, Transportation } from './types';
export function getRandomQuote() {
const quotes = inspirationalQuotes.quotes;
@ -74,3 +74,81 @@ export function isValidUrl(url: string) {
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;
transportations?: Transportation[];
notes?: Note[];
checklists?: Checklist[];
};
export type OpenStreetMapPlace = {
@ -118,3 +119,25 @@ export type Note = {
created_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">
import type { Adventure, Collection, Note, Transportation } from '$lib/types';
import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
@ -18,6 +18,9 @@
import NoteCard from '$lib/components/NoteCard.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;
console.log(data);
@ -30,6 +33,7 @@
let transportations: Transportation[] = [];
let notes: Note[] = [];
let checklists: Checklist[] = [];
let numberOfDays: number = NaN;
@ -63,84 +67,15 @@
if (collection.notes) {
notes = collection.notes;
}
if (collection.checklists) {
checklists = collection.checklists;
}
});
function deleteAdventure(event: CustomEvent<number>) {
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>) {
adventures = [event.detail, ...adventures];
isShowingCreateModal = false;
@ -489,7 +424,23 @@
</div>
{/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}
<div class="divider"></div>
<h1 class="text-center font-bold text-4xl mt-4">Itinerary by Date</h1>
{#if numberOfDays}
<p class="text-center text-lg pl-16 pr-16">Duration: {numberOfDays} days</p>
@ -499,20 +450,24 @@
collection.end_date
).toLocaleDateString('en-US', { timeZone: 'UTC' })}
</p>
<div class="divider"></div>
{#each Array(numberOfDays) as _, i}
{@const currentDate = new Date(collection.start_date)}
{@const temp = currentDate.setDate(currentDate.getDate() + i)}
{@const dateString = currentDate.toISOString().split('T')[0]}
{@const dayAdventures = groupAdventuresByDate(adventures, new Date(collection.start_date))[
dateString
]}
{@const dayAdventures = groupAdventuresByDate(
adventures,
new Date(collection.start_date),
numberOfDays
)[dateString]}
{@const dayTransportations = groupTransportationsByDate(
transportations,
new Date(collection.start_date)
new Date(collection.start_date),
numberOfDays
)[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">
Day {i + 1} - {currentDate.toLocaleDateString('en-US', { timeZone: 'UTC' })}