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
When contributing to this repository, please first discuss the change you wish to make via issue,
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.
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.
## 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.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
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.
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. **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.
3. **Pull Request**: Submit a pull request with your changes. Make sure to reference the issue you opened in the description.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
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.
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.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
In order to maintain a positive environment, we encourage the following behaviors:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
- **Inclusivity**: Use welcoming and inclusive language that fosters collaboration across all perspectives and experiences.
- **Respect**: Respect differing opinions and engage with empathy, understanding that each persons perspective is valuable.
- **Constructive Feedback**: Offer feedback that helps improve the project and allows contributors to grow from it.
- **Adventure Spirit**: Bring the same sense of curiosity, discovery, and positivity that drives AdventureLog into all interactions with the community.
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
- Personal attacks, trolling, or any form of harassment.
- Insensitive or discriminatory language, including sexualized comments or imagery.
- Spamming or misusing project spaces for personal gain.
- Publishing or using others private information without permission.
- Anything else that could be seen as disrespectful or unprofessional in a collaborative environment.
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
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.
Project maintainers have the right and responsibility to remove, edit, or
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.
We strive to foster a community that balances open collaboration with respect for all contributors.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
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.
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.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
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.
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.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/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.

View file

@ -25,10 +25,10 @@ EMAIL_BACKEND='console'
# ------------------- #
# 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'
# PGDATABASE='admin'
# PGDATABASE='adventurelog'
# PGUSER='admin'
# PGPASSWORD='admin'
# ------------------- #

View file

@ -22,7 +22,7 @@ urlpatterns = [
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
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/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 .serializers import CustomUserDetailsSerializer as PublicUserSerializer
from allauth.socialaccount.models import SocialApp
from adventures.serializers import AdventureSerializer, CollectionSerializer
from adventures.models import Adventure, Collection
User = get_user_model()
@ -79,12 +81,28 @@ class PublicUserDetailView(APIView):
},
operation_description="Get public user information."
)
def get(self, request, user_id):
user = get_object_or_404(User, uuid=user_id, public_profile=True)
def get(self, request, username):
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
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):
permission_classes = [IsAuthenticated]

View file

@ -34,7 +34,9 @@
? `${user.first_name} ${user.last_name}`
: user.username}
</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('/shared')}>{$t('navbar.shared_with_me')}</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)",
"both_passwords_required": "Both passwords are required",
"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": {
"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>