1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-18 20:39:36 +02:00

feat: Update user profile handling and enhance public user details response

This commit is contained in:
Sean Morley 2025-01-29 22:50:53 -05:00
parent b68e2dedaa
commit 85b55660f9
10 changed files with 274 additions and 212 deletions

View file

@ -1,91 +1,50 @@
# Contributing to AdventureLog # Contributing to AdventureLog
When contributing to this repository, please first discuss the change you wish to make via issue, Were excited to have you contribute to AdventureLog! To ensure that this community remains welcoming and productive for all users and developers, please follow this simple Code of Conduct.
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process ## Pull Request Process
1. Please make sure you create an issue first for your change so you can link any pull requests to this issue. There should be a clear relationship between pull requests and issues. 1. **Open an Issue First**: Discuss any changes or features you plan to implement by opening an issue. This helps to clarify your idea and ensures theres a shared understanding.
2. Update the README.md with details of changes to the interface, this includes new environment 2. **Document Changes**: If your changes impact the user interface, add new environment variables, or introduce new container configurations, make sure to update the documentation accordingly. The documentation is located in the `documentation` folder.
variables, exposed ports, useful file locations and container parameters. 3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
## Code of Conduct ## Code of Conduct
### Our Pledge ### Our Pledge
In the interest of fostering an open and welcoming environment, we as At AdventureLog, we are committed to creating a community that fosters adventure, exploration, and innovation. We encourage diverse participation and strive to maintain a space where everyone feels welcome to contribute, regardless of their background or experience level. We ask that you contribute with respect and kindness, making sure to prioritize collaboration and mutual growth.
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards ### Our Standards
Examples of behavior that contributes to creating a positive environment In order to maintain a positive environment, we encourage the following behaviors:
include:
- Using welcoming and inclusive language - **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences.
- Being respectful of differing viewpoints and experiences - **Respect**: Respect differing opinions and engage with empathy, understanding that each persons perspective is valuable.
- Gracefully accepting constructive criticism - **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it.
- Focusing on what is best for the community - **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community.
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery and unwelcome sexual attention or - Personal attacks, trolling, or any form of harassment.
advances - Insensitive or discriminatory language, including sexualized comments or imagery.
- Trolling, insulting/derogatory comments, and personal or political attacks - Spamming or misusing project spaces for personal gain.
- Public or private harassment - Publishing or using others private information without permission.
- Publishing others' private information, such as a physical or electronic - Anything else that could be seen as disrespectful or unprofessional in a collaborative environment.
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities ### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable As maintainers of AdventureLog, we are committed to enforcing this Code of Conduct and taking corrective action when necessary. This may involve moderating comments, pulling code, or banning users who engage in harmful behaviors.
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or We strive to foster a community that balances open collaboration with respect for all contributors.
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope ### Scope
This Code of Conduct applies both within project spaces and in public spaces This Code of Conduct applies in all spaces related to AdventureLog. This includes our GitHub repository, discussions, documentation, social media accounts, and events—both online and in person.
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement ### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be If you experience or witness unacceptable behavior, please report it to the project team at `contact@adventurelog.app`. All reports will be confidential and handled swiftly. The maintainers will investigate the issue and take appropriate action as needed.
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution ### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is inspired by the [Contributor Covenant](http://contributor-covenant.org), version 1.4, and adapted to fit the unique spirit of AdventureLog.
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View file

@ -25,10 +25,10 @@ EMAIL_BACKEND='console'
# ------------------- # # ------------------- #
# For Developers to start a Demo Database # For Developers to start a Demo Database
# docker run --name postgres-admin -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=admin -p 5432:5432 -d postgis/postgis:15-3.3 # docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3
# PGHOST='localhost' # PGHOST='localhost'
# PGDATABASE='admin' # PGDATABASE='adventurelog'
# PGUSER='admin' # PGUSER='admin'
# PGPASSWORD='admin' # PGPASSWORD='admin'
# ------------------- # # ------------------- #

View file

@ -22,7 +22,7 @@ urlpatterns = [
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'), path('auth/user/<str:username>/', PublicUserDetailView.as_view(), name='public-user-detail'),
path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'), path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'),
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),

View file

@ -11,6 +11,8 @@ from django.shortcuts import get_object_or_404
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .serializers import CustomUserDetailsSerializer as PublicUserSerializer from .serializers import CustomUserDetailsSerializer as PublicUserSerializer
from allauth.socialaccount.models import SocialApp from allauth.socialaccount.models import SocialApp
from adventures.serializers import AdventureSerializer, CollectionSerializer
from adventures.models import Adventure, Collection
User = get_user_model() User = get_user_model()
@ -79,12 +81,28 @@ class PublicUserDetailView(APIView):
}, },
operation_description="Get public user information." operation_description="Get public user information."
) )
def get(self, request, user_id): def get(self, request, username):
user = get_object_or_404(User, uuid=user_id, public_profile=True) print(request.user)
if request.user.username == username:
user = get_object_or_404(User, username=username)
else:
user = get_object_or_404(User, username=username, public_profile=True)
serializer = PublicUserSerializer(user)
# remove the email address from the response # remove the email address from the response
user.email = None user.email = None
serializer = PublicUserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK) # Get the users adventures and collections to include in the response
adventures = Adventure.objects.filter(user_id=user, is_public=True)
collections = Collection.objects.filter(user_id=user, is_public=True)
adventure_serializer = AdventureSerializer(adventures, many=True)
collection_serializer = CollectionSerializer(collections, many=True)
return Response({
'user': serializer.data,
'adventures': adventure_serializer.data,
'collections': collection_serializer.data
}, status=status.HTTP_200_OK)
class UserMetadataView(APIView): class UserMetadataView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]

View file

@ -34,7 +34,9 @@
? `${user.first_name} ${user.last_name}` ? `${user.first_name} ${user.last_name}`
: user.username} : user.username}
</p> </p>
<li><button on:click={() => goto('/profile')}>{$t('navbar.profile')}</button></li> <li>
<button on:click={() => goto(`/profile/${user.username}`)}>{$t('navbar.profile')}</button>
</li>
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li> <li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li> <li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li> <li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>

View file

@ -326,7 +326,11 @@
"new_password": "New Password (6+ characters)", "new_password": "New Password (6+ characters)",
"both_passwords_required": "Both passwords are required", "both_passwords_required": "Both passwords are required",
"reset_failed": "Failed to reset password", "reset_failed": "Failed to reset password",
"or_3rd_party": "Or login with a third-party service" "or_3rd_party": "Or login with a third-party service",
"no_public_adventures": "No public adventures found",
"no_public_collections": "No public collections found",
"user_adventures": "User Adventures",
"user_collections": "User Collections"
}, },
"users": { "users": {
"no_users_found": "No users found with public profiles." "no_users_found": "No users found with public profiles."

View file

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

View file

@ -1,112 +0,0 @@
<script lang="ts">
export let data;
import { t } from 'svelte-i18n';
let stats: {
visited_country_count: number;
total_regions: number;
trips_count: number;
adventure_count: number;
visited_region_count: number;
total_countries: number;
visited_city_count: number;
total_cities: number;
} | null;
stats = data.stats || null;
</script>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if data.user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={data.user.profile_pic} alt="Profile" />
</div>
</div>
{/if}
<!-- User Name -->
{#if data.user && data.user.first_name && data.user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{data.user.first_name}
{data.user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{data.user.username}</p>
<!-- Member Since -->
{#if data.user && data.user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
<!-- Stats Section -->
{#if stats}
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_region_count}/{stats.total_regions}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_city_count}/{stats.total_cities}
</div>
</div>
</div>
</div>
{/if}
</section>
<svelte:head>
<title>Profile | AdventureLog</title>
<meta name="description" content="{data.user.first_name}'s profile on AdventureLog." />
</svelte:head>

View file

@ -0,0 +1,40 @@
import { redirect, error } from '@sveltejs/kit';
import type { PageServerLoad, RequestEvent } from '../../$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
export const load: PageServerLoad = async (event: RequestEvent) => {
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
let uuid = event.params.uuid as string;
if (!uuid) {
return error(404, 'Not found');
}
// let sessionId = event.cookies.get('sessionid');
// let stats = null;
// let res = await event.fetch(`${endpoint}/api/stats/counts/`, {
// headers: {
// Cookie: `sessionid=${sessionId}`
// }
// });
// if (!res.ok) {
// console.error('Failed to fetch user stats');
// } else {
// stats = await res.json();
// }
let userData = await event.fetch(`${endpoint}/auth/user/${uuid}/`);
if (!userData.ok) {
return error(404, 'Not found');
}
let data = await userData.json();
return {
user: data.user,
adventures: data.adventures,
collections: data.collections
};
};

View file

@ -0,0 +1,180 @@
<script lang="ts">
export let data;
import AdventureCard from '$lib/components/AdventureCard.svelte';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import type { Adventure, Collection, User } from '$lib/types.js';
import { t } from 'svelte-i18n';
// let stats: {
// visited_country_count: number;
// total_regions: number;
// trips_count: number;
// adventure_count: number;
// visited_region_count: number;
// total_countries: number;
// visited_city_count: number;
// total_cities: number;
// } | null;
const user: User = data.user;
const adventures: Adventure[] = data.adventures;
const collections: Collection[] = data.collections;
// console.log(user);
// console.log(adventures);
// console.log(collections);
// stats = data.stats || null;
</script>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={user.profile_pic} alt="Profile" />
</div>
</div>
{:else}
<!-- show first last initial -->
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
{#if user.first_name && user.last_name}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.first_name}+${user.last_name}&size=250`}
alt="Profile"
/>
{:else}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.username}&size=250`}
alt="Profile"
/>
{/if}
</div>
</div>
{/if}
<!-- User Name -->
{#if user && user.first_name && user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{user.first_name}
{user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{user.username}</p>
<!-- Member Since -->
{#if user && user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
<!-- Stats Section -->
<!-- {#if stats}
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_region_count}/{stats.total_regions}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_city_count}/{stats.total_cities}
</div>
</div>
</div>
</div>
{/if} -->
<!-- Adventures Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_adventures')}
</h2>
{#if adventures && adventures.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_adventures')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard {adventure} user={null} />
{/each}
</div>
{/if}
<!-- Collections Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_collections')}
</h2>
{#if collections && collections.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_collections')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each collections as collection}
<CollectionCard {collection} type={''} />
{/each}
</div>
{/if}
</section>
<svelte:head>
<title>{user.first_name || user.username}'s Profile | AdventureLog</title>
<meta name="description" content="User Profile" />
</svelte:head>