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

feat: Refactor user detail view and enhance localization strings for multiple languages

This commit is contained in:
Sean Morley 2025-02-03 19:56:25 -05:00
parent ad5fb02ebb
commit 9c3a52ae85
14 changed files with 121 additions and 66 deletions

View file

@ -2,29 +2,42 @@ from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion from worldtravel.models import City, Region, Country, VisitedCity, VisitedRegion
from adventures.models import Adventure, Collection from adventures.models import Adventure, Collection
from users.serializers import CustomUserDetailsSerializer as PublicUserSerializer
from django.contrib.auth import get_user_model
User = get_user_model()
class StatsViewSet(viewsets.ViewSet): class StatsViewSet(viewsets.ViewSet):
""" """
A simple ViewSet for listing the stats of a user. A simple ViewSet for listing the stats of a user.
""" """
permission_classes = [IsAuthenticated] @action(detail=False, methods=['get'], url_path='counts/(?P<username>[^/.]+)')
def counts(self, request, username):
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
@action(detail=False, methods=['get']) # get the counts for the user
def counts(self, request):
adventure_count = Adventure.objects.filter( adventure_count = Adventure.objects.filter(
user_id=request.user.id).count() user_id=user.id).count()
trips_count = Collection.objects.filter( trips_count = Collection.objects.filter(
user_id=request.user.id).count() user_id=user.id).count()
visited_city_count = VisitedCity.objects.filter( visited_city_count = VisitedCity.objects.filter(
user_id=request.user.id).count() user_id=user.id).count()
total_cities = City.objects.count() total_cities = City.objects.count()
visited_region_count = VisitedRegion.objects.filter( visited_region_count = VisitedRegion.objects.filter(
user_id=request.user.id).count() user_id=user.id).count()
total_regions = Region.objects.count() total_regions = Region.objects.count()
visited_country_count = VisitedRegion.objects.filter( visited_country_count = VisitedRegion.objects.filter(
user_id=request.user.id).values('region__country').distinct().count() user_id=user.id).values('region__country').distinct().count()
total_countries = Country.objects.count() total_countries = Country.objects.count()
return Response({ return Response({
'adventure_count': adventure_count, 'adventure_count': adventure_count,

View file

@ -82,7 +82,6 @@ class PublicUserDetailView(APIView):
operation_description="Get public user information." operation_description="Get public user information."
) )
def get(self, request, username): def get(self, request, username):
print(request.user)
if request.user.username == username: if request.user.username == username:
user = get_object_or_404(User, username=username) user = get_object_or_404(User, username=username)
else: else:

View file

@ -10,7 +10,6 @@
ReverseGeocode ReverseGeocode
} from '$lib/types'; } from '$lib/types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { addToast } from '$lib/toasts'; import { addToast } from '$lib/toasts';
import { deserialize } from '$app/forms'; import { deserialize } from '$app/forms';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -1257,7 +1256,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
{#if immichIntegration} {#if immichIntegration}
<ImmichSelect <ImmichSelect
adventure={adventure} {adventure}
on:fetchImage={(e) => { on:fetchImage={(e) => {
url = e.detail; url = e.detail;
fetchImage(); fetchImage();

View file

@ -234,7 +234,8 @@
"images": "Bilder", "images": "Bilder",
"primary": "Primär", "primary": "Primär",
"upload": "Hochladen", "upload": "Hochladen",
"view_attachment": "Anhang anzeigen" "view_attachment": "Anhang anzeigen",
"of": "von"
}, },
"home": { "home": {
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
@ -302,7 +303,11 @@
"both_passwords_required": "Beide Passwörter sind erforderlich", "both_passwords_required": "Beide Passwörter sind erforderlich",
"new_password": "Neues Passwort", "new_password": "Neues Passwort",
"reset_failed": "Passwort konnte nicht zurückgesetzt werden", "reset_failed": "Passwort konnte nicht zurückgesetzt werden",
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an" "or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an",
"no_public_adventures": "Keine öffentlichen Abenteuer gefunden",
"no_public_collections": "Keine öffentlichen Sammlungen gefunden",
"user_adventures": "Benutzerabenteuer",
"user_collections": "Benutzersammlungen"
}, },
"users": { "users": {
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden." "no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."

View file

@ -165,6 +165,7 @@
"delete_collection_success": "Collection deleted successfully!", "delete_collection_success": "Collection deleted successfully!",
"delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.", "delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.",
"cancel": "Cancel", "cancel": "Cancel",
"of": "of",
"delete_collection": "Delete Collection", "delete_collection": "Delete Collection",
"delete_adventure": "Delete Adventure", "delete_adventure": "Delete Adventure",
"adventure_delete_success": "Adventure deleted successfully!", "adventure_delete_success": "Adventure deleted successfully!",

View file

@ -281,7 +281,8 @@
"primary": "Primario", "primary": "Primario",
"upload": "Subir", "upload": "Subir",
"view_attachment": "Ver archivo adjunto", "view_attachment": "Ver archivo adjunto",
"attachment_name": "Nombre del archivo adjunto" "attachment_name": "Nombre del archivo adjunto",
"of": "de"
}, },
"worldtravel": { "worldtravel": {
"all": "Todo", "all": "Todo",
@ -326,7 +327,11 @@
"both_passwords_required": "Se requieren ambas contraseñas", "both_passwords_required": "Se requieren ambas contraseñas",
"new_password": "Nueva contraseña", "new_password": "Nueva contraseña",
"reset_failed": "No se pudo restablecer la contraseña", "reset_failed": "No se pudo restablecer la contraseña",
"or_3rd_party": "O inicie sesión con un servicio de terceros" "or_3rd_party": "O inicie sesión con un servicio de terceros",
"no_public_adventures": "No se encontraron aventuras públicas",
"no_public_collections": "No se encontraron colecciones públicas",
"user_adventures": "Aventuras de usuario",
"user_collections": "Colecciones de usuarios"
}, },
"users": { "users": {
"no_users_found": "No se encontraron usuarios con perfiles públicos." "no_users_found": "No se encontraron usuarios con perfiles públicos."

View file

@ -234,7 +234,8 @@
"images": "Images", "images": "Images",
"primary": "Primaire", "primary": "Primaire",
"upload": "Télécharger", "upload": "Télécharger",
"view_attachment": "Voir la pièce jointe" "view_attachment": "Voir la pièce jointe",
"of": "de"
}, },
"home": { "home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité", "desc_1": "Découvrez, planifiez et explorez en toute simplicité",
@ -302,7 +303,11 @@
"both_passwords_required": "Les deux mots de passe sont requis", "both_passwords_required": "Les deux mots de passe sont requis",
"new_password": "Nouveau mot de passe", "new_password": "Nouveau mot de passe",
"reset_failed": "Échec de la réinitialisation du mot de passe", "reset_failed": "Échec de la réinitialisation du mot de passe",
"or_3rd_party": "Ou connectez-vous avec un service tiers" "or_3rd_party": "Ou connectez-vous avec un service tiers",
"no_public_adventures": "Aucune aventure publique trouvée",
"no_public_collections": "Aucune collection publique trouvée",
"user_adventures": "Aventures utilisateur",
"user_collections": "Collections d'utilisateurs"
}, },
"users": { "users": {
"no_users_found": "Aucun utilisateur trouvé avec des profils publics." "no_users_found": "Aucun utilisateur trouvé avec des profils publics."

View file

@ -234,7 +234,8 @@
"images": "Immagini", "images": "Immagini",
"primary": "Primario", "primary": "Primario",
"upload": "Caricamento", "upload": "Caricamento",
"view_attachment": "Visualizza allegato" "view_attachment": "Visualizza allegato",
"of": "Di"
}, },
"home": { "home": {
"desc_1": "Scopri, pianifica ed esplora con facilità", "desc_1": "Scopri, pianifica ed esplora con facilità",
@ -302,7 +303,11 @@
"both_passwords_required": "Sono necessarie entrambe le password", "both_passwords_required": "Sono necessarie entrambe le password",
"new_password": "Nuova parola d'ordine", "new_password": "Nuova parola d'ordine",
"reset_failed": "Impossibile reimpostare la password", "reset_failed": "Impossibile reimpostare la password",
"or_3rd_party": "Oppure accedi con un servizio di terze parti" "or_3rd_party": "Oppure accedi con un servizio di terze parti",
"no_public_adventures": "Nessuna avventura pubblica trovata",
"no_public_collections": "Nessuna collezione pubblica trovata",
"user_adventures": "Avventure utente",
"user_collections": "Collezioni utente"
}, },
"users": { "users": {
"no_users_found": "Nessun utente trovato con profili pubblici." "no_users_found": "Nessun utente trovato con profili pubblici."

View file

@ -234,7 +234,8 @@
"images": "Afbeeldingen", "images": "Afbeeldingen",
"primary": "Primair", "primary": "Primair",
"upload": "Uploaden", "upload": "Uploaden",
"view_attachment": "Bijlage bekijken" "view_attachment": "Bijlage bekijken",
"of": "van"
}, },
"home": { "home": {
"desc_1": "Ontdek, plan en verken met gemak", "desc_1": "Ontdek, plan en verken met gemak",
@ -302,7 +303,11 @@
"both_passwords_required": "Beide wachtwoorden zijn vereist", "both_passwords_required": "Beide wachtwoorden zijn vereist",
"new_password": "Nieuw wachtwoord", "new_password": "Nieuw wachtwoord",
"reset_failed": "Kan het wachtwoord niet opnieuw instellen", "reset_failed": "Kan het wachtwoord niet opnieuw instellen",
"or_3rd_party": "Of log in met een service van derden" "or_3rd_party": "Of log in met een service van derden",
"no_public_adventures": "Geen openbare avonturen gevonden",
"no_public_collections": "Geen openbare collecties gevonden",
"user_adventures": "Gebruikersavonturen",
"user_collections": "Gebruikerscollecties"
}, },
"users": { "users": {
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen." "no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."

View file

@ -281,7 +281,8 @@
"images": "Obrazy", "images": "Obrazy",
"primary": "Podstawowy", "primary": "Podstawowy",
"upload": "Wgrywać", "upload": "Wgrywać",
"view_attachment": "Zobacz załącznik" "view_attachment": "Zobacz załącznik",
"of": "z"
}, },
"worldtravel": { "worldtravel": {
"country_list": "Lista krajów", "country_list": "Lista krajów",
@ -326,7 +327,11 @@
"both_passwords_required": "Obydwa hasła są wymagane", "both_passwords_required": "Obydwa hasła są wymagane",
"new_password": "Nowe hasło", "new_password": "Nowe hasło",
"reset_failed": "Nie udało się zresetować hasła", "reset_failed": "Nie udało się zresetować hasła",
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej" "or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej",
"no_public_adventures": "Nie znaleziono publicznych przygód",
"no_public_collections": "Nie znaleziono publicznych kolekcji",
"user_adventures": "Przygody użytkowników",
"user_collections": "Kolekcje użytkowników"
}, },
"users": { "users": {
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami." "no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."

View file

@ -234,7 +234,8 @@
"images": "Bilder", "images": "Bilder",
"primary": "Primär", "primary": "Primär",
"upload": "Ladda upp", "upload": "Ladda upp",
"view_attachment": "Visa bilaga" "view_attachment": "Visa bilaga",
"of": "av"
}, },
"home": { "home": {
"desc_1": "Upptäck, planera och utforska med lätthet", "desc_1": "Upptäck, planera och utforska med lätthet",
@ -326,7 +327,11 @@
"both_passwords_required": "Båda lösenorden krävs", "both_passwords_required": "Båda lösenorden krävs",
"new_password": "Nytt lösenord", "new_password": "Nytt lösenord",
"reset_failed": "Det gick inte att återställa lösenordet", "reset_failed": "Det gick inte att återställa lösenordet",
"or_3rd_party": "Eller logga in med en tredjepartstjänst" "or_3rd_party": "Eller logga in med en tredjepartstjänst",
"no_public_adventures": "Inga offentliga äventyr hittades",
"no_public_collections": "Inga offentliga samlingar hittades",
"user_adventures": "Användaräventyr",
"user_collections": "Användarsamlingar"
}, },
"users": { "users": {
"no_users_found": "Inga användare hittades med offentliga profiler." "no_users_found": "Inga användare hittades med offentliga profiler."

View file

@ -234,7 +234,8 @@
"images": "图片", "images": "图片",
"primary": "基本的", "primary": "基本的",
"upload": "上传", "upload": "上传",
"view_attachment": "查看附件" "view_attachment": "查看附件",
"of": "的"
}, },
"home": { "home": {
"desc_1": "轻松发现、规划和探索", "desc_1": "轻松发现、规划和探索",
@ -302,7 +303,11 @@
"both_passwords_required": "两个密码都需要", "both_passwords_required": "两个密码都需要",
"new_password": "新密码", "new_password": "新密码",
"reset_failed": "重置密码失败", "reset_failed": "重置密码失败",
"or_3rd_party": "或者使用第三方服务登录" "or_3rd_party": "或者使用第三方服务登录",
"no_public_adventures": "找不到公共冒险",
"no_public_collections": "找不到公共收藏",
"user_adventures": "用户冒险",
"user_collections": "用户收集"
}, },
"worldtravel": { "worldtravel": {
"all": "全部", "all": "全部",

View file

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

View file

@ -5,26 +5,21 @@
import type { Adventure, Collection, User } from '$lib/types.js'; import type { Adventure, Collection, User } from '$lib/types.js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
// let stats: { let stats: {
// visited_country_count: number; visited_country_count: number;
// total_regions: number; total_regions: number;
// trips_count: number; trips_count: number;
// adventure_count: number; adventure_count: number;
// visited_region_count: number; visited_region_count: number;
// total_countries: number; total_countries: number;
// visited_city_count: number; visited_city_count: number;
// total_cities: number; total_cities: number;
// } | null; } | null;
const user: User = data.user; const user: User = data.user;
const adventures: Adventure[] = data.adventures; const adventures: Adventure[] = data.adventures;
const collections: Collection[] = data.collections; const collections: Collection[] = data.collections;
stats = data.stats || null;
// console.log(user);
// console.log(adventures);
// console.log(collections);
// stats = data.stats || null;
</script> </script>
<section class="min-h-screen bg-base-100 py-8 px-4"> <section class="min-h-screen bg-base-100 py-8 px-4">
@ -83,7 +78,7 @@
</div> </div>
<!-- Stats Section --> <!-- Stats Section -->
<!-- {#if stats} {#if stats}
<div class="divider my-8"></div> <div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary"> <h2 class="text-2xl font-bold text-center mb-6 text-primary">
@ -105,35 +100,44 @@
<div class="stat"> <div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div> <div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center"> <div class="stat-value text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}% {stats.visited_country_count}
</div> </div>
<div class="stat-desc text-center"> <div class="stat-desc text-center">
{stats.visited_country_count}/{stats.total_countries} {Math.round((stats.visited_country_count / stats.total_countries) * 100)}% {$t(
'adventures.of'
)}
{stats.total_countries}
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div> <div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center"> <div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}% {stats.visited_region_count}
</div> </div>
<div class="stat-desc text-center"> <div class="stat-desc text-center">
{stats.visited_region_count}/{stats.total_regions} {Math.round((stats.visited_region_count / stats.total_regions) * 100)}% {$t(
'adventures.of'
)}
{stats.total_regions}
</div> </div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div> <div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center"> <div class="stat-value text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}% {stats.visited_city_count}
</div> </div>
<div class="stat-desc text-center"> <div class="stat-desc text-center">
{stats.visited_city_count}/{stats.total_cities} {Math.round((stats.visited_city_count / stats.total_cities) * 100)}% {$t(
'adventures.of'
)}
{stats.total_cities}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} --> {/if}
<!-- Adventures Section --> <!-- Adventures Section -->
<div class="divider my-8"></div> <div class="divider my-8"></div>