Skip to content

Commit

Permalink
Merge pull request #131 from seanmorley15/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
seanmorley15 authored Jul 18, 2024
2 parents 5b886fd + 7051fa7 commit 2434e76
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 7 deletions.
3 changes: 2 additions & 1 deletion backend/server/adventures/urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription
from .views import AdventureViewSet, CollectionViewSet, StatsViewSet, GenerateDescription, ActivityTypesView

router = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures')
router.register(r'collections', CollectionViewSet, basename='collections')
router.register(r'stats', StatsViewSet, basename='stats')
router.register(r'generate', GenerateDescription, basename='generate')
router.register(r'activity-types', ActivityTypesView, basename='activity-types')


urlpatterns = [
Expand Down
40 changes: 39 additions & 1 deletion backend/server/adventures/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ def all(self, request):
serializer = self.get_serializer(queryset, many=True)

return Response(serializer.data)

@action(detail=False, methods=['get'])
def search(self, request):
query = self.request.query_params.get('query', '')
queryset = Adventure.objects.filter(
(Q(name__icontains=query) | Q(description__icontains=query) | Q(location__icontains=query) | Q(activity_types__icontains=query)) &
(Q(user_id=request.user.id) | Q(is_public=True))
)
queryset = self.apply_sorting(queryset)
adventures = self.paginate_and_respond(queryset, request)
return adventures

def paginate_and_respond(self, queryset, request):
paginator = self.pagination_class()
Expand Down Expand Up @@ -330,4 +341,31 @@ def img(self, request):
if extract.get('original') is None:
return Response({"error": "No image found"}, status=400)
return Response(extract["original"])



class ActivityTypesView(viewsets.ViewSet):
permission_classes = [IsAuthenticated]

@action(detail=False, methods=['get'])
def types(self, request):
"""
Retrieve a list of distinct activity types for adventures associated with the current user.
Args:
request (HttpRequest): The HTTP request object.
Returns:
Response: A response containing a list of distinct activity types.
"""
types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct()

allTypes = []

for i in types:
if not i:
continue
for x in i:
if x and x not in allTypes:
allTypes.append(x)

return Response(allTypes)
91 changes: 91 additions & 0 deletions frontend/src/lib/components/ActivityComplete.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<script lang="ts">
import { onMount } from 'svelte';
export let activities: string[] | undefined | null;
let allActivities: string[] = [];
let inputVal: string = '';
if (activities == null || activities == undefined) {
activities = [];
}
onMount(async () => {
let res = await fetch('/api/activity-types/types/');
let data = await res.json();
if (data) {
allActivities = data;
}
});
function addActivity() {
if (inputVal && activities) {
const trimmedInput = inputVal.trim();
if (trimmedInput && !activities.includes(trimmedInput)) {
activities = [...activities, trimmedInput];
inputVal = '';
}
}
}
function removeActivity(item: string) {
if (activities) {
activities = activities.filter((activity) => activity !== item);
}
}
$: filteredItems = allActivities.filter(function (activity) {
return (
activity.toLowerCase().includes(inputVal.toLowerCase()) &&
(!activities || !activities.includes(activity))
);
});
</script>

<div class="relative">
<input
type="text"
class="input input-bordered w-full"
placeholder="Add an activity"
bind:value={inputVal}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addActivity();
}
}}
/>
{#if inputVal && filteredItems.length > 0}
<ul class="absolute z-10 w-full bg-base-100 shadow-lg max-h-60 overflow-auto">
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#each filteredItems as item}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<li
class="p-2 hover:bg-base-200 cursor-pointer"
on:click={() => {
inputVal = item;
addActivity();
}}
>
{item}
</li>
{/each}
</ul>
{/if}
</div>

<div class="mt-2">
<ul class="space-y-2">
{#if activities}
{#each activities as activity}
<li class="flex items-center justify-between bg-base-200 p-2 rounded">
{activity}
<button class="btn btn-sm btn-error" on:click={() => removeActivity(activity)}>
Remove
</button>
</li>
{/each}
{/if}
</ul>
</div>
1 change: 1 addition & 0 deletions frontend/src/lib/components/Avatar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<p class="text-lg ml-4 font-bold">Hi, {user.first_name} {user.last_name}</p>
<li><button on:click={() => goto('/profile')}>Profile</button></li>
<li><button on:click={() => goto('/adventures')}>My Adventures</button></li>
<li><button on:click={() => goto('/activities')}>My Activities</button></li>
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
<form method="post">
<li><button formaction="/?/logout">Logout</button></li>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lib/components/EditAdventure.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import Earth from '~icons/mdi/earth';
import Wikipedia from '~icons/mdi/wikipedia';
import ImageFetcher from './ImageFetcher.svelte';
import ActivityComplete from './ActivityComplete.svelte';
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
Expand Down Expand Up @@ -213,9 +214,11 @@
type="text"
id="activity_types"
name="activity_types"
hidden
bind:value={adventureToEdit.activity_types}
class="input input-bordered w-full max-w-xs mt-1"
/>
<ActivityComplete bind:activities={adventureToEdit.activity_types} />
</div>
<div class="mb-2">
<label for="image">Image </label><br />
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/lib/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import Water from '~icons/mdi/water';
import AboutModal from './AboutModal.svelte';
import Avatar from './Avatar.svelte';
import { page } from '$app/stores';
let query: string = '';
let isAboutModalOpen: boolean = false;
Expand All @@ -22,6 +25,22 @@
document.documentElement.setAttribute('data-theme', theme);
}
};
const searchGo = async (e: Event) => {
e.preventDefault();
let reload: boolean = false;
if ($page.url.pathname === '/search') {
reload = true;
}
if (query) {
await goto(`/search?query=${query}`);
if (reload) {
window.location.reload();
}
}
};
</script>

{#if isAboutModalOpen}
Expand Down Expand Up @@ -96,6 +115,24 @@
<li>
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
</li>
<label class="input input-bordered flex items-center gap-2">
<form>
<input type="text" bind:value={query} class="grow" placeholder="Search" />
</form>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
fill-rule="evenodd"
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
clip-rule="evenodd"
/>
</svg>
</label>
<button on:click={searchGo} type="submit" class="btn btn-neutral">Search</button>
{/if}

{#if !data.user}
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/lib/components/NewAdventure.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
export let type: string = 'visited';
import Wikipedia from '~icons/mdi/wikipedia';
import ClipboardList from '~icons/mdi/clipboard-list';
import ActivityComplete from './ActivityComplete.svelte';
let newAdventure: Adventure = {
id: NaN,
Expand Down Expand Up @@ -218,17 +220,18 @@
</div>
</div>
<div class="mb-2">
<label for="activity_types"
>Activity Types <iconify-icon icon="mdi:clipboard-list" class="text-xl -mb-1"
></iconify-icon></label
<label for="activityTypes"
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="text"
name="activity_types"
id="activity_types"
name="activity_types"
hidden
bind:value={newAdventure.activity_types}
class="input input-bordered w-full max-w-xs mt-1"
/>
<ActivityComplete bind:activities={newAdventure.activity_types} />
</div>
<div class="mb-2">
<label for="rating"
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspirationalQuotes from './json/quotes.json';
import type { Adventure, Collection } from './types';

export function getRandomQuote() {
const quotes = inspirationalQuotes.quotes;
Expand All @@ -19,3 +20,48 @@ export function checkLink(link: string) {
return 'http://' + link + '.com';
}
}

export async function exportData() {
let res = await fetch('/api/adventures/all');
let adventures = (await res.json()) as Adventure[];

res = await fetch('/api/collections/all');
let collections = (await res.json()) as Collection[];

res = await fetch('/api/visitedregion');
let visitedRegions = await res.json();

const data = {
adventures,
collections,
visitedRegions
};

async function convertImages() {
const promises = data.adventures.map(async (adventure, i) => {
if (adventure.image) {
const res = await fetch(adventure.image);
const blob = await res.blob();
const base64 = await blobToBase64(blob);
adventure.image = base64;
data.adventures[i].image = adventure.image;
}
});

await Promise.all(promises);
}

function blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
}

await convertImages();

const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
return URL.createObjectURL(blob);
}
26 changes: 26 additions & 0 deletions frontend/src/routes/activities/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';

export const load = (async (event) => {
if (!event.locals.user) {
return redirect(302, '/login');
}
let allActivities: string[] = [];
let res = await fetch(`${endpoint}/api/activity-types/types/`, {
headers: {
'Content-Type': 'application/json',
Cookie: `${event.cookies.get('auth')}`
}
});
let data = await res.json();
if (data) {
allActivities = data;
}
return {
props: {
activities: allActivities
}
};
}) satisfies PageServerLoad;
33 changes: 33 additions & 0 deletions frontend/src/routes/activities/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let activities: string[] = data.props.activities;
</script>

<!-- make a table with pinned rows -->
<table class="table table-compact">
<thead>
<tr>
<th>Activity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each activities as activity}
<tr>
<td>{activity}</td>
<td>
<!-- <button
class="btn btn-sm btn-error"
on:click={() => {
activities = activities.filter((a) => a !== activity);
}}>Remove</button
> -->
</td>
</tr>
{/each}
</tbody>
</table>
<!-- </ul> -->
Loading

0 comments on commit 2434e76

Please sign in to comment.