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:
commit
4a293798eb
47 changed files with 1368 additions and 311 deletions
2
frontend/src/app.d.ts
vendored
2
frontend/src/app.d.ts
vendored
|
@ -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 {}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
118
frontend/src/lib/components/ShareModal.svelte
Normal file
118
frontend/src/lib/components/ShareModal.svelte
Normal 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>
|
|
@ -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
|
||||
|
|
51
frontend/src/lib/components/UserCard.svelte
Normal file
51
frontend/src/lib/components/UserCard.svelte
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: '/',
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
83
frontend/src/routes/auth/[...path]/+server.ts
Normal file
83
frontend/src/routes/auth/[...path]/+server.ts
Normal 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 });
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
26
frontend/src/routes/shared/+page.server.ts
Normal file
26
frontend/src/routes/shared/+page.server.ts
Normal 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;
|
27
frontend/src/routes/shared/+page.svelte
Normal file
27
frontend/src/routes/shared/+page.svelte
Normal 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}
|
29
frontend/src/routes/user/[uuid]/+page.server.ts
Normal file
29
frontend/src/routes/user/[uuid]/+page.server.ts
Normal 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;
|
35
frontend/src/routes/user/[uuid]/+page.svelte
Normal file
35
frontend/src/routes/user/[uuid]/+page.svelte
Normal 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>
|
26
frontend/src/routes/users/+page.server.ts
Normal file
26
frontend/src/routes/users/+page.server.ts
Normal 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;
|
25
frontend/src/routes/users/+page.svelte
Normal file
25
frontend/src/routes/users/+page.svelte
Normal 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>
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue