1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-06 21:55:18 +02:00

Merge pull request #112 from seanmorley15/development

Development
This commit is contained in:
Sean Morley 2024-07-10 18:25:00 -04:00 committed by GitHub
commit a33dd130aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 405 additions and 25 deletions

View file

@ -44,8 +44,8 @@ class Adventure(models.Model):
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) 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: if self.type != self.trip.type:
raise ValidationError('Adventure type must match trip type. Trip type: ' + self.trip.type + ' Adventure type: ' + self.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: if self.type == 'featured' and not self.is_public:
raise ValidationError('Featured adventures must be public. Adventure: ' + self.name) raise ValidationError('Featured adventures must be public. Adventure: ' + self.name)
def __str__(self): def __str__(self):
return self.name return self.name

View file

@ -1,10 +1,11 @@
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, TripViewSet from .views import AdventureViewSet, TripViewSet, StatsViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures') router.register(r'adventures', AdventureViewSet, basename='adventures')
router.register(r'trips', TripViewSet, basename='trips') router.register(r'trips', TripViewSet, basename='trips')
router.register(r'stats', StatsViewSet, basename='stats')
urlpatterns = [ urlpatterns = [
# Include the router under the 'api/' prefix # Include the router under the 'api/' prefix

View file

@ -2,6 +2,7 @@ from rest_framework.decorators import action
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from .models import Adventure, Trip from .models import Adventure, Trip
from worldtravel.models import VisitedRegion, Region, Country
from .serializers import AdventureSerializer, TripSerializer from .serializers import AdventureSerializer, TripSerializer
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
@ -47,7 +48,7 @@ class TripViewSet(viewsets.ModelViewSet):
def get_queryset(self): def get_queryset(self):
return Trip.objects.filter( return Trip.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) Q(is_public=True) | Q(user_id=self.request.user.id)
).select_related( ).prefetch_related(
Prefetch('adventure_set', queryset=Adventure.objects.filter( Prefetch('adventure_set', queryset=Adventure.objects.filter(
Q(is_public=True) | Q(user_id=self.request.user.id) Q(is_public=True) | Q(user_id=self.request.user.id)
)) ))
@ -73,3 +74,33 @@ class TripViewSet(viewsets.ModelViewSet):
trips = self.get_queryset().filter(type='featured', is_public=True) trips = self.get_queryset().filter(type='featured', is_public=True)
serializer = self.get_serializer(trips, many=True) serializer = self.get_serializer(trips, many=True)
return Response(serializer.data) return Response(serializer.data)
class StatsViewSet(viewsets.ViewSet):
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def counts(self, request):
visited_count = Adventure.objects.filter(
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(
user_id=request.user.id).count()
visited_region_count = VisitedRegion.objects.filter(
user_id=request.user.id).count()
total_regions = Region.objects.count()
country_count = VisitedRegion.objects.filter(
user_id=request.user.id).values('region__country').distinct().count()
total_countries = Country.objects.count()
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,
'country_count': country_count,
'total_countries': total_countries
})

View file

@ -4,7 +4,7 @@ sidebar_position: 1
# Docker 🐋 # Docker 🐋
Docker is the perffered way to run AdventureLog on your local machine. It is a lightweight containerization technology that allows you to run applications in isolated environments called containers. Docker is the preferred way to run AdventureLog on your local machine. It is a lightweight containerization technology that allows you to run applications in isolated environments called containers.
**Note**: This guide mainly focuses on installation with a linux based host machine, but the steps are similar for other operating systems. **Note**: This guide mainly focuses on installation with a linux based host machine, but the steps are similar for other operating systems.
## Prerequisites ## Prerequisites

View file

@ -10,8 +10,6 @@
export let visit_id: number | undefined | null; export let visit_id: number | undefined | null;
console.log(visit_id);
async function markVisited() { async function markVisited() {
let res = await fetch(`/worldtravel?/markVisited`, { let res = await fetch(`/worldtravel?/markVisited`, {
method: 'POST', method: 'POST',
@ -47,6 +45,7 @@
if (res.ok) { if (res.ok) {
visited = false; visited = false;
addToast('info', `Visit to ${region.name} removed`); addToast('info', `Visit to ${region.name} removed`);
dispatch('remove', null);
} else { } else {
console.error('Failed to remove visit'); console.error('Failed to remove visit');
} }

View file

@ -0,0 +1,57 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Calendar from '~icons/mdi/calendar';
import MapMarker from '~icons/mdi/map-marker';
import Launch from '~icons/mdi/launch';
import TrashCanOutline from '~icons/mdi/trash-can-outline';
import { goto } from '$app/navigation';
import type { Trip } from '$lib/types';
const dispatch = createEventDispatcher();
// export let type: String;
export let trip: Trip;
// function remove() {
// dispatch("remove", trip.id);
// }
// function edit() {}
// function add() {
// dispatch("add", trip);
// }
// // TODO: Implement markVisited function
// function markVisited() {
// console.log(trip.id);
// dispatch("markVisited", trip);
// }
</script>
<div
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-primary-content shadow-xl overflow-hidden text-base-content"
>
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{trip.name}</h2>
{#if trip.date && trip.date !== ''}
<div class="inline-flex items-center">
<Calendar class="w-5 h-5 mr-1" />
<p class="ml-1">{trip.date}</p>
</div>
{/if}
{#if trip.location && trip.location !== ''}
<div class="inline-flex items-center">
<MapMarker class="w-5 h-5 mr-1" />
<p class="ml-1">{trip.location}</p>
</div>
{/if}
<div class="card-actions justify-end">
<button class="btn btn-secondary"><TrashCanOutline class="w-5 h-5 mr-1" /></button>
<button class="btn btn-primary" on:click={() => goto(`/trip/${trip.id}`)}
><Launch class="w-5 h-5 mr-1" /></button
>
</div>
</div>
</div>

View file

@ -37,7 +37,7 @@ export type Country = {
export type Region = { export type Region = {
id: number; id: number;
name: string; name: string;
country_id: number; country: number;
}; };
export type VisitedRegion = { export type VisitedRegion = {
@ -53,3 +53,14 @@ export type Point = {
}; };
name: string; name: string;
}; };
export type Trip = {
id: number;
user_id: number;
name: string;
type: string;
location: string;
date: string;
is_public: boolean;
adventures: Adventure[];
};

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Lost from '$lib/assets/undraw_lost.svg';
</script>
<h1>{$page.status}: {$page.error?.message}</h1>
{#if $page.status === 404}
<div
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"
>
<div class="mx-auto max-w-md text-center">
<img src={Lost} alt="Lost in the forest" />
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Oops, looks like you've wandered off the beaten path.
</h1>
<p class="mt-4 text-muted-foreground">
We couldn't find the page you were looking for. Don't worry, we can help you find your way
back.ry, we can
</p>
<div class="mt-6 flex flex-col items-center gap-4 sm:flex-row">
<button class="btn btn-neutral" on:click={() => goto('/')}>Go to Homepage</button>
</div>
</div>
</div>
{/if}

View file

@ -6,6 +6,8 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import EditAdventure from '$lib/components/EditAdventure.svelte'; import EditAdventure from '$lib/components/EditAdventure.svelte';
import Lost from '$lib/assets/undraw_lost.svg';
export let data: PageData; export let data: PageData;
console.log(data); console.log(data);
@ -80,7 +82,10 @@
</div> </div>
</div> </div>
<h1 class="text-center font-bold text-4xl mb-4">Visited Adventures</h1> {#if adventures.length > 0}
<h1 class="text-center font-bold text-4xl mb-4">Planned Adventures</h1>
{/if}
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
{#each adventures as adventure} {#each adventures as adventure}
<AdventureCard type="planned" {adventure} on:delete={deleteAdventure} on:edit={editAdventure} /> <AdventureCard type="planned" {adventure} on:delete={deleteAdventure} on:edit={editAdventure} />
@ -88,7 +93,19 @@
</div> </div>
{#if adventures.length === 0} {#if adventures.length === 0}
<div class="flex justify-center items-center h-96"> <div
<p class="text-2xl text-primary-content">No planned adventures yet!</p> class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
>
<div class="mx-auto max-w-md text-center">
<div class="flex items-center justify-center">
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
No planned adventures found
</h1>
<p class="mt-4 text-muted-foreground">
There are no adventures to display. Add some using the plus button at the bottom right!
</p>
</div>
</div> </div>
{/if} {/if}

View file

@ -1,11 +1,27 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { PageServerLoad, RequestEvent } from '../$types'; import type { PageServerLoad, RequestEvent } from '../$types';
import { PUBLIC_SERVER_URL } from '$env/static/public';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load: PageServerLoad = async (event: RequestEvent) => { export const load: PageServerLoad = async (event: RequestEvent) => {
if (!event.locals.user) { if (!event.locals.user || !event.cookies.get('auth')) {
return redirect(302, '/login'); return redirect(302, '/login');
} }
let stats = null;
let res = await event.fetch(`${endpoint}/api/stats/counts/`, {
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (!res.ok) {
console.error('Failed to fetch user stats');
} else {
stats = await res.json();
}
return { return {
user: event.locals.user user: event.locals.user,
stats
}; };
}; };

View file

@ -1,5 +1,23 @@
<script lang="ts"> <script lang="ts">
export let data; export let data;
let stats: {
country_count: number;
featured_count: number;
planned_count: number;
total_regions: number;
trips_count: number;
visited_count: number;
visited_region_count: number;
total_countries: number;
} | null;
if (data.stats) {
stats = data.stats;
} else {
stats = null;
}
console.log(stats);
</script> </script>
<!-- <!--
@ -36,3 +54,52 @@
<p class="ml-1 text-xl">{new Date(data.user.date_joined).toLocaleDateString()}</p> <p class="ml-1 text-xl">{new Date(data.user.date_joined).toLocaleDateString()}</p>
</div> </div>
{/if} {/if}
{#if stats}
<!-- divider -->
<div class="divider pr-8 pl-8"></div>
<h1 class="text-center text-2xl font-bold mt-8 mb-2">User Stats</h1>
<div class="flex justify-center items-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">Completed Adventures</div>
<div class="stat-value text-center">{stats.visited_count}</div>
<!-- <div class="stat-desc">Jan 1st - Feb 1st</div> -->
</div>
<div class="stat">
<div class="stat-title">Planned Adventures</div>
<div class="stat-value text-center">{stats.planned_count}</div>
<!-- <div class="stat-desc">↗︎ 400 (22%)</div> -->
</div>
<div class="stat">
<div class="stat-title">Trips</div>
<div class="stat-value text-center">{stats.trips_count}</div>
<!-- <div class="stat-desc">↘︎ 90 (14%)</div> -->
</div>
<div class="stat">
<div class="stat-title">Visited Countries</div>
<div class="stat-value text-center">
{Math.round((stats.country_count / stats.total_countries) * 100)}%
</div>
<div class="stat-desc">
{stats.country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">Visited Regions</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
</div>
<div class="stat-desc">
{stats.visited_region_count}/{stats.total_regions}
</div>
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,31 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { PUBLIC_SERVER_URL } from '$env/static/public';
export const load = (async (event) => {
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
if (!event.locals.user || !event.cookies.get('auth')) {
return redirect(302, '/login');
} else {
let res = await event.fetch(`${endpoint}/api/trips/`, {
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (res.ok) {
let data = await res.json();
console.log(data);
return {
props: {
trips: data
}
};
} else {
return {
status: res.status,
error: 'Failed to load trips'
};
}
}
return {};
}) satisfies PageServerLoad;

View file

@ -0,0 +1,74 @@
<script lang="ts">
import type { Trip } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import Lost from '$lib/assets/undraw_lost.svg';
import { goto } from '$app/navigation';
import TripCard from '$lib/components/TripCard.svelte';
export let data: PageData;
let trips: Trip[];
let notFound: boolean = false;
let noTrips: boolean = false;
onMount(() => {
if (data.props && data.props.trips?.length > 0) {
trips = data.props.trips;
} else if (data.props && data.props.trips?.length === 0) {
noTrips = true;
} else {
notFound = true;
}
});
console.log(data);
</script>
{#if notFound}
<div
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
>
<div class="mx-auto max-w-md text-center">
<div class="flex items-center justify-center">
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Adventure not Found
</h1>
<p class="mt-4 text-muted-foreground">
The adventure you were looking for could not be found. Please try a different adventure or
check back later.
</p>
<div class="mt-6">
<button class="btn btn-primary" on:click={() => goto('/')}>Homepage</button>
</div>
</div>
</div>
{/if}
{#if noTrips}
<div
class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
>
<div class="mx-auto max-w-md text-center">
<div class="flex items-center justify-center">
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
No Trips Found
</h1>
<p class="mt-4 text-muted-foreground">
There are no trips to display. Please try again later.
</p>
</div>
</div>
{/if}
{#if trips && !notFound}
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
{#each trips as trip (trip.id)}
<TripCard {trip} />
{/each}
</div>
{/if}

View file

@ -6,6 +6,8 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import EditAdventure from '$lib/components/EditAdventure.svelte'; import EditAdventure from '$lib/components/EditAdventure.svelte';
import Lost from '$lib/assets/undraw_lost.svg';
export let data: PageData; export let data: PageData;
console.log(data); console.log(data);
@ -80,7 +82,10 @@
</div> </div>
</div> </div>
<h1 class="text-center font-bold text-4xl mb-4">Visited Adventures</h1> {#if adventures.length > 0}
<h1 class="text-center font-bold text-4xl mb-4">Visited Adventures</h1>
{/if}
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
{#each adventures as adventure} {#each adventures as adventure}
<AdventureCard type="visited" {adventure} on:delete={deleteAdventure} on:edit={editAdventure} /> <AdventureCard type="visited" {adventure} on:delete={deleteAdventure} on:edit={editAdventure} />
@ -88,7 +93,19 @@
</div> </div>
{#if adventures.length === 0} {#if adventures.length === 0}
<div class="flex justify-center items-center h-96"> <div
<p class="text-2xl text-primary-content">No visited adventures yet!</p> class="flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8 -mt-20"
>
<div class="mx-auto max-w-md text-center">
<div class="flex items-center justify-center">
<img src={Lost} alt="Lost" class="w-1/2" />
</div>
<h1 class="mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
No visited adventures found
</h1>
<p class="mt-4 text-muted-foreground">
There are no adventures to display. Add some using the plus button at the bottom right!
</p>
</div>
</div> </div>
{/if} {/if}

View file

@ -67,8 +67,6 @@ export const actions: Actions = {
removeVisited: async (event) => { removeVisited: async (event) => {
const body = await event.request.json(); const body = await event.request.json();
console.log(body);
if (!body || !body.visitId) { if (!body || !body.visitId) {
return { return {
status: 400 status: 400

View file

@ -1,5 +1,6 @@
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Region, VisitedRegion } from '$lib/types'; import type { Country, Region, VisitedRegion } from '$lib/types';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -9,6 +10,7 @@ export const load = (async (event) => {
let regions: Region[] = []; let regions: Region[] = [];
let visitedRegions: VisitedRegion[] = []; let visitedRegions: VisitedRegion[] = [];
let country: Country;
let res = await fetch(`${endpoint}/api/${id}/regions/`, { let res = await fetch(`${endpoint}/api/${id}/regions/`, {
method: 'GET', method: 'GET',
@ -18,7 +20,7 @@ export const load = (async (event) => {
}); });
if (!res.ok) { if (!res.ok) {
console.error('Failed to fetch regions'); console.error('Failed to fetch regions');
return { status: 500 }; return redirect(302, '/404');
} else { } else {
regions = (await res.json()) as Region[]; regions = (await res.json()) as Region[];
} }
@ -36,10 +38,24 @@ export const load = (async (event) => {
visitedRegions = (await res.json()) as VisitedRegion[]; visitedRegions = (await res.json()) as VisitedRegion[];
} }
res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, {
method: 'GET',
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (!res.ok) {
console.error('Failed to fetch country');
return { status: 500 };
} else {
country = (await res.json()) as Country;
}
return { return {
props: { props: {
regions, regions,
visitedRegions visitedRegions,
country
} }
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View file

@ -5,11 +5,27 @@
export let data: PageData; export let data: PageData;
let regions: Region[] = data.props?.regions || []; let regions: Region[] = data.props?.regions || [];
let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || []; let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || [];
const country = data.props?.country || null;
console.log(data); console.log(data);
let numRegions: number = regions.length;
let numVisitedRegions: number = visitedRegions.length;
</script> </script>
<h1 class="text-center font-bold text-4xl mb-4">Regions</h1> <h1 class="text-center font-bold text-4xl mb-4">Regions in {country?.name}</h1>
<div class="flex items-center justify-center mb-4">
<div class="stats shadow bg-base-300">
<div class="stat">
<div class="stat-title">Region Stats</div>
<div class="stat-value">{numVisitedRegions}/{numRegions} Visited</div>
{#if numRegions === numVisitedRegions}
<div class="stat-desc">You've visited all regions in {country?.name} 🎉!</div>
{:else}
<div class="stat-desc">Keep exploring!</div>
{/if}
</div>
</div>
</div>
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
{#each regions as region} {#each regions as region}
@ -18,8 +34,10 @@
visited={visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)} visited={visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)}
on:visit={(e) => { on:visit={(e) => {
visitedRegions = [...visitedRegions, e.detail]; visitedRegions = [...visitedRegions, e.detail];
numVisitedRegions++;
}} }}
visit_id={visitedRegions.find((visitedRegion) => visitedRegion.region === region.id)?.id} visit_id={visitedRegions.find((visitedRegion) => visitedRegion.region === region.id)?.id}
on:remove={() => numVisitedRegions--}
/> />
{/each} {/each}
</div> </div>