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:
parent
b68e2dedaa
commit
85b55660f9
10 changed files with 274 additions and 212 deletions
|
@ -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,
|
We’re 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 there’s 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 person’s 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/
|
|
||||||
|
|
|
@ -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'
|
||||||
# ------------------- #
|
# ------------------- #
|
|
@ -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'),
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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>
|
|
40
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal file
40
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
180
frontend/src/routes/profile/[uuid]/+page.svelte
Normal file
180
frontend/src/routes/profile/[uuid]/+page.svelte
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue