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

Allow for Sharing of Collections to other Public Users

This commit is contained in:
Sean Morley 2024-09-10 17:16:05 -04:00 committed by GitHub
commit 4a293798eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1368 additions and 311 deletions

View file

@ -13,6 +13,8 @@ declare global {
date_joined: string | null;
is_staff: boolean;
profile_pic: string | null;
uuid: string;
public_profile: boolean;
} | null;
}
// interface PageData {}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import type { Adventure, User } from '$lib/types';
import type { Adventure, Collection, User } from '$lib/types';
const dispatch = createEventDispatcher();
import Launch from '~icons/mdi/launch';
@ -21,8 +21,8 @@
import ImageDisplayModal from './ImageDisplayModal.svelte';
export let type: string;
export let user: User | null;
export let collection: Collection | null = null;
let isCollectionModalOpen: boolean = false;
let isWarningModalOpen: boolean = false;
@ -161,7 +161,7 @@
{/if}
<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 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-neutral text-neutral-content shadow-xl"
>
<figure>
{#if adventure.images && adventure.images.length > 0}
@ -209,19 +209,17 @@
</button>
</div>
<div>
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
{#if adventure.type == 'visited'}
<div class="badge badge-primary">Visited</div>
{:else if user?.pk == adventure.user_id && adventure.type == 'planned'}
{:else if adventure.type == 'planned'}
<div class="badge badge-secondary">Planned</div>
{:else if (user?.pk !== adventure.user_id && adventure.type == 'planned') || adventure.type == 'visited'}
<div class="badge badge-secondary">Adventure</div>
{:else if user?.pk == adventure.user_id && adventure.type == 'lodging'}
{:else if adventure.type == 'lodging'}
<div class="badge badge-success">Lodging</div>
{:else if adventure.type == 'dining'}
<div class="badge badge-accent">Dining</div>
{/if}
<div class="badge badge-neutral">{adventure.is_public ? 'Public' : 'Private'}</div>
<div class="badge badge-secondary">{adventure.is_public ? 'Public' : 'Private'}</div>
</div>
{#if adventure.location && adventure.location !== ''}
<div class="inline-flex items-center">
@ -254,9 +252,9 @@
<div class="card-actions justify-end mt-2">
<!-- action options dropdown -->
{#if type != 'link'}
{#if user?.pk == adventure.user_id}
{#if adventure.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-neutral">
<div tabindex="0" role="button" class="btn btn-neutral-200">
<DotsHorizontal class="w-6 h-6" />
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
@ -272,18 +270,18 @@
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
<FileDocumentEdit class="w-6 h-6" />Edit {keyword}
</button>
{#if adventure.type == 'visited'}
{#if adventure.type == 'visited' && user?.pk == adventure.user_id}
<button class="btn btn-neutral mb-2" on:click={changeType('planned')}
><FormatListBulletedSquare class="w-6 h-6" />Change to Plan</button
>
{/if}
{#if adventure.type == 'planned'}
{#if adventure.type == 'planned' && user?.pk == adventure.user_id}
<button class="btn btn-neutral mb-2" on:click={changeType('visited')}
><CheckBold class="w-6 h-6" />Mark Visited</button
>
{/if}
<!-- remove from adventure -->
{#if adventure.collection && (adventure.type == 'visited' || adventure.type == 'planned')}
{#if adventure.collection && (adventure.type == 'visited' || adventure.type == 'planned') && user?.pk == adventure.user_id}
<button class="btn btn-neutral mb-2" on:click={removeFromCollection}
><LinkVariantRemove class="w-6 h-6" />Remove from Collection</button
>
@ -309,8 +307,9 @@
</ul>
</div>
{:else}
<button class="btn btn-neutral mb-2" on:click={() => goto(`/adventures/${adventure.id}`)}
><Launch class="w-6 h-6" /></button
<button
class="btn btn-neutral-200 mb-2"
on:click={() => goto(`/adventures/${adventure.id}`)}><Launch class="w-6 h-6" /></button
>
{/if}
{/if}

View file

@ -627,22 +627,24 @@
</button>
{/if}
</div>
<div>
<div class="mt-2">
<div>
<label for="is_public"
>Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={adventure.is_public}
/>
{#if !collection_id}
<div>
<div class="mt-2">
<div>
<label for="is_public"
>Public <Earth class="inline-block -mt-1 mb-1 w-6 h-6" /></label
><br />
<input
type="checkbox"
class="toggle toggle-primary"
id="is_public"
name="is_public"
bind:checked={adventure.is_public}
/>
</div>
</div>
</div>
</div>
{/if}
</div>
<div class="divider"></div>
<h2 class="text-2xl font-semibold mb-2 mt-2">Location Information</h2>

View file

@ -12,7 +12,7 @@
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content rounded-full w-10 ml-4">
<div class="bg-neutral rounded-full text-neutral-200 w-10 ml-4">
{#if user.profile_pic}
<img src={user.profile_pic} alt="User Profile" />
{:else}
@ -24,7 +24,7 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-primary-content mt-2 rounded-box w-52"
class="dropdown-content z-[1] text-neutral-200 menu p-2 shadow bg-neutral mt-2 rounded-box w-52"
>
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-missing-attribute -->
@ -32,6 +32,7 @@
<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('/shared')}>Shared With Me</button></li>
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
<form method="post">
<li><button formaction="/?/logout">Logout</button></li>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { addToast } from '$lib/toasts';
import type { Checklist, User } from '$lib/types';
import type { Checklist, Collection, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@ -10,6 +10,7 @@
export let checklist: Checklist;
export let user: User | null = null;
export let collection: Collection | null = null;
function editChecklist() {
dispatch('edit', checklist);
@ -29,7 +30,7 @@
</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"
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
>
<div class="card-body">
<div class="flex justify-between">
@ -51,10 +52,10 @@
<!-- <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={editChecklist}>
<button class="btn btn-neutral-200 mb-2" on:click={editChecklist}>
<Launch class="w-6 h-6" />Open
</button>
{#if checklist.user_id == user?.pk}
{#if checklist.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Checklist"

View file

@ -33,7 +33,7 @@
{
name: newItem,
is_checked: newStatus,
id: 0,
id: '',
user_id: 0,
checklist: 0,
created_at: '',
@ -135,7 +135,7 @@
<p class="font-semibold text-md mb-2">Editing checklist {initialName}</p>
{/if}
{#if (checklist && user?.pk == checklist?.user_id) || !checklist}
{#if (checklist && user?.pk == checklist?.user_id) || (user && collection && collection.shared_with.includes(user.uuid)) || !checklist}
<form on:submit|preventDefault>
<div class="form-control mb-2">
<label for="name">Name</label>

View file

@ -16,10 +16,12 @@
import DotsHorizontal from '~icons/mdi/dots-horizontal';
import TrashCan from '~icons/mdi/trashcan';
import DeleteWarning from './DeleteWarning.svelte';
import ShareModal from './ShareModal.svelte';
const dispatch = createEventDispatcher();
export let type: String | undefined | null;
let isShareModalOpen: boolean = false;
// export let type: String;
@ -77,8 +79,12 @@
/>
{/if}
{#if isShareModalOpen}
<ShareModal {collection} on:close={() => (isShareModalOpen = false)} />
{/if}
<div
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-primary-content shadow-xl text-base-content"
class="card min-w-max lg:w-96 md:w-80 sm:w-60 xs:w-40 bg-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<div class="flex justify-between">
@ -90,7 +96,7 @@
</button>
</div>
<div class="inline-flex gap-2 mb-2">
<div class="badge badge-neutral">{collection.is_public ? 'Public' : 'Private'}</div>
<div class="badge badge-secondary">{collection.is_public ? 'Public' : 'Private'}</div>
{#if collection.is_archived}
<div class="badge badge-warning">Archived</div>
{/if}
@ -117,7 +123,7 @@
</button>
{:else}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-neutral">
<div tabindex="0" role="button" class="btn btn-neutral-200">
<DotsHorizontal class="w-6 h-6" />
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
@ -125,7 +131,7 @@
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow"
>
{#if type != 'link'}
{#if type != 'link' && type != 'viewonly'}
<button
class="btn btn-neutral mb-2"
on:click={() => goto(`/collections/${collection.id}`)}
@ -135,6 +141,9 @@
<button class="btn btn-neutral mb-2" on:click={editAdventure}>
<FileDocumentEdit class="w-6 h-6" />Edit Collection
</button>
<button class="btn btn-neutral mb-2" on:click={() => (isShareModalOpen = true)}>
<FileDocumentEdit class="w-6 h-6" />Share
</button>
{/if}
{#if collection.is_archived}
<button class="btn btn-neutral mb-2" on:click={() => archiveCollection(false)}>
@ -153,6 +162,13 @@
><TrashCan class="w-6 h-6" />Delete</button
>
{/if}
{#if type == 'viewonly'}
<button
class="btn btn-neutral mb-2"
on:click={() => goto(`/collections/${collection.id}`)}
><Launch class="w-5 h-5 mr-1" />Open Details</button
>
{/if}
</ul>
</div>
{/if}

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getFlag } from '$lib';
import { continentCodeToString, getFlag } from '$lib';
import type { Country } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@ -13,7 +13,7 @@
</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"
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
>
<figure>
<!-- svelte-ignore a11y-img-redundant-alt -->
@ -21,6 +21,7 @@
</figure>
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{country.name}</h2>
<div class="badge badge-primary">{continentCodeToString(country.continent)}</div>
<div class="card-actions justify-end">
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
<button class="btn btn-primary" on:click={nav}>Open</button>

View file

@ -11,7 +11,9 @@
import Flower from '~icons/mdi/flower';
import Water from '~icons/mdi/water';
import AboutModal from './AboutModal.svelte';
import AccountMultiple from '~icons/mdi/account-multiple';
import Avatar from './Avatar.svelte';
import PaletteOutline from '~icons/mdi/palette-outline';
import { page } from '$app/stores';
let query: string = '';
@ -81,6 +83,9 @@
<li>
<button on:click={() => goto('/map')}>Map</button>
</li>
<li>
<button on:click={() => goto('/users')}>Users</button>
</li>
{/if}
{#if !data.user}
@ -133,6 +138,11 @@
<li>
<button class="btn btn-neutral" on:click={() => goto('/map')}>Map</button>
</li>
<li>
<button class="btn btn-neutral" on:click={() => goto('/users')}
><AccountMultiple /></button
>
</li>
{/if}
{#if !data.user}
@ -184,6 +194,10 @@
on:click={() => (window.location.href = 'https://docs.adventurelog.app/')}
>Documentation</button
>
<button
class="btn btn-sm mt-2"
on:click={() => (window.location.href = 'https://discord.gg/wRbQ9Egr8C')}>Discord</button
>
<p class="font-bold m-4 text-lg">Theme Selection</p>
<form method="POST" use:enhance={submitUpdateTheme}>
<li>
@ -202,7 +216,12 @@
</li>
<li>
<button formaction="/?/setTheme&theme=forest">Forest<Forest class="w-6 h-6" /></button>
<button formaction="/?/setTheme&theme=garden">Garden<Flower class="w-6 h-6" /></button>
<button formaction="/?/setTheme&theme=aestheticLight"
>Aesthetic Light<PaletteOutline class="w-6 h-6" /></button
>
<button formaction="/?/setTheme&theme=aestheticDark"
>Aesthetic Dark<PaletteOutline class="w-6 h-6" /></button
>
<button formaction="/?/setTheme&theme=aqua">Aqua<Water class="w-6 h-6" /></button>
</li>
</form>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { addToast } from '$lib/toasts';
import type { Note, User } from '$lib/types';
import type { Collection, Note, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
@ -11,6 +11,7 @@
export let note: Note;
export let user: User | null = null;
export let collection: Collection | null = null;
function editNote() {
dispatch('edit', note);
@ -30,7 +31,7 @@
</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"
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md overflow-hidden bg-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<div class="flex justify-between">
@ -52,10 +53,10 @@
<!-- <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}>
<button class="btn btn-neutral-200 mb-2" on:click={editNote}>
<Launch class="w-6 h-6" />Open
</button>
{#if note.user_id == user?.pk}
{#if note.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
<button
id="delete_adventure"
data-umami-event="Delete Adventure"

View file

@ -4,6 +4,7 @@
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import ShareModal from './ShareModal.svelte';
let modal: HTMLDialogElement;
export let note: Note | null = null;
@ -113,7 +114,7 @@
<p class="font-semibold text-md mb-2">Editing note {initialName}</p>
{/if}
{#if (note && user?.pk == note?.user_id) || !note}
{#if (note && user?.pk == note?.user_id) || (collection && user && collection.shared_with.includes(user.uuid)) || !note}
<form on:submit|preventDefault>
<div class="form-control mb-2">
<label for="name">Name</label>

View file

@ -54,7 +54,7 @@
</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"
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-neutral text-neutral-content shadow-xl overflow-hidden"
>
<div class="card-body">
{#if region.name_en && region.name !== region.name_en}

View file

@ -0,0 +1,118 @@
<script lang="ts">
import type { Collection, User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
import UserCard from './UserCard.svelte';
import { addToast } from '$lib/toasts';
let modal: HTMLDialogElement;
export let collection: Collection;
let allUsers: User[] = [];
let sharedWithUsers: User[] = [];
let notSharedWithUsers: User[] = [];
async function share(user: User) {
let res = await fetch(`/api/collections/${collection.id}/share/${user.uuid}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
sharedWithUsers = sharedWithUsers.concat(user);
collection.shared_with.push(user.uuid);
notSharedWithUsers = notSharedWithUsers.filter((u) => u.uuid !== user.uuid);
addToast('success', `Shared ${collection.name} with ${user.first_name} ${user.last_name}`);
}
}
async function unshare(user: User) {
let res = await fetch(`/api/collections/${collection.id}/unshare/${user.uuid}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (res.ok) {
notSharedWithUsers = notSharedWithUsers.concat(user);
collection.shared_with = collection.shared_with.filter((u) => u !== user.uuid);
sharedWithUsers = sharedWithUsers.filter((u) => u.uuid !== user.uuid);
addToast('success', `Unshared ${collection.name} with ${user.first_name} ${user.last_name}`);
}
}
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
let res = await fetch(`/auth/users/`);
if (res.ok) {
let data = await res.json();
allUsers = data;
sharedWithUsers = allUsers.filter((user) => collection.shared_with.includes(user.uuid));
notSharedWithUsers = allUsers.filter((user) => !collection.shared_with.includes(user.uuid));
console.log(sharedWithUsers);
console.log(notSharedWithUsers);
}
});
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
dispatch('close');
}
}
</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 w-11/12 max-w-5xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">Share {collection.name}</h3>
<p class="py-1">Share this collection with other users.</p>
<div class="divider"></div>
<h3 class="font-bold text-md">Shared With</h3>
<ul>
{#each sharedWithUsers as user}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
<UserCard
{user}
shared_with={collection.shared_with}
sharing={true}
on:share={(event) => share(event.detail)}
on:unshare={(event) => unshare(event.detail)}
/>
</div>
{/each}
{#if sharedWithUsers.length === 0}
<p class="text-neutral-content">No users shared with</p>
{/if}
</ul>
<div class="divider"></div>
<h3 class="font-bold text-md">Not Shared With</h3>
<ul>
{#each notSharedWithUsers as user}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
<UserCard
{user}
shared_with={collection.shared_with}
sharing={true}
on:share={(event) => share(event.detail)}
on:unshare={(event) => unshare(event.detail)}
/>
</div>
{/each}
{#if notSharedWithUsers.length === 0}
<p class="text-neutral-content">No users not shared with</p>
{/if}
</ul>
<button class="btn btn-primary mt-4" on:click={close}>Close</button>
</div>
</dialog>

View file

@ -1,22 +1,17 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Launch from '~icons/mdi/launch';
import TrashCanOutline from '~icons/mdi/trash-can-outline';
import FileDocumentEdit from '~icons/mdi/file-document-edit';
import { goto } from '$app/navigation';
import type { Collection, Transportation, User } from '$lib/types';
import { addToast } from '$lib/toasts';
import Plus from '~icons/mdi/plus';
import ArrowDownThick from '~icons/mdi/arrow-down-thick';
const dispatch = createEventDispatcher();
export let transportation: Transportation;
export let user: User | null = null;
export let collection: Collection | null = null;
function editTransportation() {
dispatch('edit', transportation);
@ -40,7 +35,7 @@
</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 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-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<h2 class="card-title overflow-ellipsis">{transportation.name}</h2>
@ -64,7 +59,7 @@
{/if}
</div>
{#if user?.pk === transportation.user_id}
{#if transportation.user_id == user?.pk || (collection && user && collection.shared_with.includes(user.uuid))}
<div class="card-actions justify-end">
<button on:click={deleteTransportation} class="btn btn-secondary"
><TrashCanOutline class="w-5 h-5 mr-1" /></button

View file

@ -0,0 +1,51 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { continentCodeToString, getFlag } from '$lib';
import type { User } from '$lib/types';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Calendar from '~icons/mdi/calendar';
export let sharing: boolean = false;
export let shared_with: string[] | undefined = undefined;
export let user: User;
</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-neutral text-neutral-content shadow-xl"
>
<div class="card-body">
<div>
{#if user.profile_pic}
<div class="avatar">
<div class="w-24 rounded-full">
<img src={user.profile_pic} alt={user.username} />
</div>
</div>
{/if}
<h2 class="card-title overflow-ellipsis">{user.first_name} {user.last_name}</h2>
</div>
<p class="text-sm text-neutral-content">{user.username}</p>
{#if user.is_staff}
<div class="badge badge-primary">Admin</div>
{/if}
<!-- member since -->
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 mr-1" />
<p class="text-sm text-neutral-content">
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
</p>
</div>
<div class="card-actions justify-end">
{#if !sharing}
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>View</button>
{:else if shared_with && !shared_with.includes(user.uuid)}
<button class="btn btn-primary" on:click={() => dispatch('share', user)}>Share</button>
{:else}
<button class="btn btn-primary" on:click={() => dispatch('unshare', user)}>Unshare</button>
{/if}
</div>
</div>
</div>

View file

@ -1,5 +1,4 @@
export let appVersion = 'Web v0.6.0';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.5.2';
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.6.0';
export let appTitle = 'AdventureLog';
export let copyrightYear = '2024';
// config for the frontend

View file

@ -1,5 +1,5 @@
import inspirationalQuotes from './json/quotes.json';
import type { Adventure, Checklist, Collection, Note, Transportation } from './types';
import type { Adventure, Checklist, Collection, Note, Transportation, User } from './types';
export function getRandomQuote() {
const quotes = inspirationalQuotes.quotes;
@ -177,3 +177,24 @@ export function groupChecklistsByDate(
return groupedChecklists;
}
export function continentCodeToString(code: string) {
switch (code) {
case 'AF':
return 'Africa';
case 'AN':
return 'Antarctica';
case 'AS':
return 'Asia';
case 'EU':
return 'Europe';
case 'NA':
return 'North America';
case 'OC':
return 'Oceania';
case 'SA':
return 'South America';
default:
return 'Unknown';
}
}

View file

@ -7,6 +7,8 @@ export type User = {
date_joined: string | null;
is_staff: boolean;
profile_pic: string | null;
uuid: string;
public_profile: boolean;
};
export type Adventure = {
@ -78,6 +80,7 @@ export type Collection = {
notes?: Note[];
checklists?: Checklist[];
is_archived?: boolean;
shared_with: string[];
};
export type OpenStreetMapPlace = {

View file

@ -4,21 +4,20 @@
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">
<h1 class="text-center text-5xl font-extrabold mt-2">
{$page.status}: {$page.error?.message}
</h1>
<h1 class="mt-4 text-xl font-bold tracking-tight text-foreground">
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>
<p class="mt-4 text-muted-foreground">We couldn't find the page you were looking for.</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>

View file

@ -9,9 +9,18 @@ export const actions: Actions = {
// change the theme only if it is one of the allowed themes
if (
theme &&
['light', 'dark', 'night', 'retro', 'forest', 'aqua', 'forest', 'garden', 'emerald'].includes(
theme
)
[
'light',
'dark',
'night',
'retro',
'forest',
'aqua',
'forest',
'aestheticLight',
'aestheticDark',
'emerald'
].includes(theme)
) {
cookies.set('colortheme', theme, {
path: '/',

View file

@ -99,72 +99,5 @@ export const actions: Actions = {
status: 204
};
}
},
addToCollection: async (event) => {
const id = event.params as { id: string };
const adventureId = id.id;
const formData = await event.request.formData();
const trip_id = formData.get('collection_id');
if (!trip_id) {
return {
status: 400,
error: { message: 'Missing collection id' }
};
}
if (!event.locals.user) {
const refresh = event.cookies.get('refresh');
let auth = event.cookies.get('auth');
if (!refresh) {
return {
status: 401,
body: { message: 'Unauthorized' }
};
}
let res = await tryRefreshToken(refresh);
if (res) {
auth = res;
event.cookies.set('auth', auth, {
httpOnly: true,
sameSite: 'lax',
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
path: '/'
});
} else {
return {
status: 401,
body: { message: 'Unauthorized' }
};
}
}
if (!adventureId) {
return {
status: 400,
error: new Error('Bad request')
};
}
let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}/`, {
method: 'PATCH',
headers: {
Cookie: `${event.cookies.get('auth')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ collection: trip_id })
});
let res2 = await res.json();
console.log(res2);
if (!res.ok) {
return {
status: res.status,
error: new Error('Failed to delete adventure')
};
} else {
return {
status: 204
};
}
}
};

View file

@ -96,11 +96,13 @@
{/if}
{#if adventure}
<div class="fixed bottom-4 right-4 z-[999]">
<button class="btn m-1 size-16 btn-primary" on:click={() => (isEditModalOpen = true)}
><ClipboardList class="w-8 h-8" /></button
>
</div>
{#if data.user && data.user.pk == adventure.user_id}
<div class="fixed bottom-4 right-4 z-[999]">
<button class="btn m-1 size-16 btn-primary" on:click={() => (isEditModalOpen = true)}
><ClipboardList class="w-8 h-8" /></button
>
</div>
{/if}
<div class="flex flex-col min-h-dvh">
<main class="flex-1">
<div class="max-w-5xl mx-auto p-4 md:p-6 lg:p-8">

View file

@ -0,0 +1,83 @@
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
import { json } from '@sveltejs/kit';
/** @type {import('./$types').RequestHandler} */
export async function GET({ url, params, request, fetch, cookies }) {
// add the param format = json to the url or add additional if anothre param is already present
if (url.search) {
url.search = url.search + '&format=json';
} else {
url.search = '?format=json';
}
return handleRequest(url, params, request, fetch, cookies);
}
/** @type {import('./$types').RequestHandler} */
export async function POST({ url, params, request, fetch, cookies }) {
return handleRequest(url, params, request, fetch, cookies, true);
}
export async function PATCH({ url, params, request, fetch, cookies }) {
return handleRequest(url, params, request, fetch, cookies, true);
}
export async function PUT({ url, params, request, fetch, cookies }) {
return handleRequest(url, params, request, fetch, cookies, true);
}
export async function DELETE({ url, params, request, fetch, cookies }) {
return handleRequest(url, params, request, fetch, cookies, true);
}
// Implement other HTTP methods as needed (PUT, DELETE, etc.)
async function handleRequest(
url: any,
params: any,
request: any,
fetch: any,
cookies: any,
requreTrailingSlash: boolean | undefined = false
) {
const path = params.path;
let targetUrl = `${endpoint}/auth/${path}${url.search}`;
if (requreTrailingSlash && !targetUrl.endsWith('/')) {
targetUrl += '/';
}
const headers = new Headers(request.headers);
const authCookie = cookies.get('auth');
if (authCookie) {
headers.set('Cookie', `${authCookie}`);
}
try {
const response = await fetch(targetUrl, {
method: request.method,
headers: headers,
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined
});
if (response.status === 204) {
// For 204 No Content, return a response with no body
return new Response(null, {
status: 204,
headers: response.headers
});
}
const responseData = await response.text();
return new Response(responseData, {
status: response.status,
headers: response.headers
});
} catch (error) {
console.error('Error forwarding request:', error);
return json({ error: 'Internal Server Error' }, { status: 500 });
}
}

View file

@ -88,16 +88,18 @@
return;
} else {
let adventure = event.detail;
let formData = new FormData();
formData.append('collection_id', collection.id.toString());
let res = await fetch(`/adventures/${adventure.id}?/addToCollection`, {
method: 'POST',
body: formData // Remove the Content-Type header
let res = await fetch(`/api/adventures/${adventure.id}/`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ collection: collection.id.toString() })
});
if (res.ok) {
console.log('Adventure added to collection');
adventure = await res.json();
adventures = [...adventures, adventure];
} else {
console.log('Error adding adventure to collection');
@ -404,6 +406,7 @@
type={adventure.type}
{adventure}
on:typeChange={changeType}
{collection}
/>
{/each}
</div>
@ -423,6 +426,7 @@
transportationToEdit = event.detail;
isTransportationEditModalOpen = true;
}}
{collection}
/>
{/each}
</div>
@ -442,6 +446,7 @@
on:delete={(event) => {
notes = notes.filter((n) => n.id != event.detail);
}}
{collection}
/>
{/each}
</div>
@ -461,6 +466,7 @@
checklistToEdit = event.detail;
isShowingChecklistModal = true;
}}
{collection}
/>
{/each}
</div>

View file

@ -45,6 +45,7 @@ export const actions: Actions = {
let first_name = formData.get('first_name') as string | null | undefined;
let last_name = formData.get('last_name') as string | null | undefined;
let profile_pic = formData.get('profile_pic') as File | null | undefined;
let public_profile = formData.get('public_profile') as string | null | undefined | boolean;
const resCurrent = await fetch(`${endpoint}/auth/user/`, {
headers: {
@ -56,6 +57,13 @@ export const actions: Actions = {
return fail(resCurrent.status, await resCurrent.json());
}
if (public_profile === 'on') {
public_profile = true;
} else {
public_profile = false;
}
console.log(public_profile);
let currentUser = (await resCurrent.json()) as User;
if (username === currentUser.username || !username) {
@ -84,6 +92,7 @@ export const actions: Actions = {
if (profile_pic) {
formDataToSend.append('profile_pic', profile_pic);
}
formDataToSend.append('public_profile', public_profile.toString());
let res = await fetch(`${endpoint}/auth/user/`, {
method: 'PATCH',

View file

@ -111,6 +111,24 @@
id="profile_pic"
class="file-input file-input-bordered w-full max-w-xs mb-2"
/><br />
<div class="form-control">
<div
class="tooltip tooltip-info"
data-tip="With a public profile, users can share collections with you and view your profile on the users page."
>
<label class="label cursor-pointer">
<span class="label-text">Public Profile</span>
<input
id="public_profile"
name="public_profile"
type="checkbox"
class="toggle"
checked={user.public_profile}
/>
</label>
</div>
</div>
<button class="py-2 mt-2 px-4 btn btn-primary">Update</button>
</form>
</div>

View file

@ -0,0 +1,26 @@
import { 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) => {
if (!event.locals.user) {
return redirect(302, '/login');
} else {
let res = await fetch(`${serverEndpoint}/api/collections/shared/`, {
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (!res.ok) {
return redirect(302, '/login');
} else {
return {
props: {
collections: await res.json()
}
};
}
}
}) satisfies PageServerLoad;

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import type { Collection } from '$lib/types';
import type { PageData } from './$types';
export let data: PageData;
console.log(data);
let collections: Collection[] = data.props.collections;
</script>
{#if collections.length > 0}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each collections as collection}
<CollectionCard type="viewonly" {collection} />
{/each}
</div>
{:else}
<p class="text-center font-semibold text-xl mt-6">
No collections found that are shared with you.
{#if data.user && !data.user?.public_profile}
<p>In order to allow users to share with you, you need your profile set to public.</p>
<button class="btn btn-neutral mt-4" on:click={() => goto('/settings')}>Go to Settings</button
>
{/if}
</p>
{/if}

View file

@ -0,0 +1,29 @@
import { 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) => {
if (!event.cookies.get('auth')) {
return redirect(302, '/login');
}
const uuid = event.params.uuid;
if (!uuid) {
return redirect(302, '/users');
}
let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, {
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (!res.ok) {
return redirect(302, '/users');
} else {
const data = await res.json();
return {
props: {
user: data
}
};
}
}) satisfies PageServerLoad;

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const user = data.props.user;
console.log(user);
</script>
{#if user.profile_pic}
<div class="avatar flex items-center justify-center mt-4">
<div class="w-48 rounded-md">
<img src={user.profile_pic} alt={user.username} />
</div>
</div>
{/if}
<h1 class="text-center font-semibold text-4xl mt-4">{user.first_name} {user.last_name}</h1>
<h2 class="text-center font-semibold text-2xl">{user.username}</h2>
<div class="flex justify-center mt-4">
{#if user.is_staff}
<div class="badge badge-primary">Admin</div>
{/if}
</div>
<div class="flex justify-center mt-4">
<p class="text-sm text-neutral-content">
{user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''}
</p>
</div>
<svelte:head>
<title>{user.username} | AdventureLog</title>
<meta name="description" content="View your adventure collections." />
</svelte:head>

View file

@ -0,0 +1,26 @@
import { 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) => {
if (!event.cookies.get('auth')) {
return redirect(302, '/login');
}
const res = await fetch(`${serverEndpoint}/auth/users/`, {
headers: {
Cookie: `${event.cookies.get('auth')}`
}
});
if (!res.ok) {
return redirect(302, '/login');
} else {
const data = await res.json();
return {
props: {
users: data
}
};
}
}) satisfies PageServerLoad;

View file

@ -0,0 +1,25 @@
<script lang="ts">
import UserCard from '$lib/components/UserCard.svelte';
import type { User } from '$lib/types';
import type { PageData } from './$types';
export let data: PageData;
let users: User[] = data.props.users;
console.log(users);
</script>
<h1 class="text-center font-bold text-4xl mb-4">AdventureLog Users</h1>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each users as user (user.uuid)}
<UserCard {user} />
{/each}
</div>
{#if users.length === 0}
<p class="text-center">No users found with public profiles.</p>
{/if}
<svelte:head>
<title>Users</title>
<meta name="description" content="View your adventure collections." />
</svelte:head>

View file

@ -3,16 +3,44 @@
import type { Country } from '$lib/types';
import type { PageData } from './$types';
let searchQuery: string = '';
let filteredCountries: Country[] = [];
export let data: PageData;
console.log(data);
const countries: Country[] = data.props?.countries || [];
$: {
// if query is empty, show all countries
if (searchQuery === '') {
filteredCountries = countries;
} else {
// otherwise, filter countries by name
filteredCountries = countries.filter((country) =>
country.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
}
</script>
<h1 class="text-center font-bold text-4xl mb-4">Country List</h1>
<h1 class="text-center font-bold text-4xl">Country List</h1>
<!-- result count -->
<p class="text-center mb-4">
{filteredCountries.length} countries found
</p>
<div class="flex items-center justify-center mb-4">
<input
type="text"
placeholder="Search"
class="input input-bordered w-full max-w-xs"
bind:value={searchQuery}
/>
</div>
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
{#each countries as country}
{#each filteredCountries as country}
<CountryCard {country} />
<!-- <p>Name: {item.name}, Continent: {item.continent}</p> -->
{/each}

View file

@ -15,7 +15,52 @@ export default {
'aqua',
'emerald',
{
garden: {
aestheticDark: {
primary: '#3e5747',
'primary-focus': '#2f4236',
'primary-content': '#e9e7e7',
secondary: '#547b82',
'secondary-focus': '#3d5960',
'secondary-content': '#c1dfe5',
accent: '#8b6161',
'accent-focus': '#6e4545',
'accent-content': '#f2eaea',
neutral: '#2b2a2a',
'neutral-focus': '#272525',
'neutral-content': '#e9e7e7',
'base-100': '#121212', // Dark background
'base-200': '#1d1d1d',
'base-300': '#292929',
'base-content': '#e9e7e7', // Light content on dark background
// set bg-primary-content
'bg-base': '#121212',
'bg-base-content': '#e9e7e7',
info: '#3b7ecb',
success: '#007766',
warning: '#d4c111',
error: '#e64a19',
'--rounded-box': '1rem',
'--rounded-btn': '.5rem',
'--rounded-badge': '1.9rem',
'--animation-btn': '.25s',
'--animation-input': '.2s',
'--btn-text-case': 'uppercase',
'--navbar-padding': '.5rem',
'--border-btn': '1px',
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
},
aestheticLight: {
primary: '#5a7c65',
'primary-focus': '#48604f',
'primary-content': '#dcd5d5',