From 74e68c32dc76d4b0880af5b5dfdff71f0f27fc55 Mon Sep 17 00:00:00 2001 From: Valknar XXX Date: Tue, 28 Oct 2025 12:54:45 +0100 Subject: [PATCH] feat: implement user profile pages with comment avatar links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added user profile feature allowing authenticated users to view profiles of other users. Key changes: **New Routes:** - `/users/[id]/+page.server.ts` - Server-side load function with authentication guard and user data fetching - `/users/[id]/+page.svelte` - User profile UI component displaying avatar, stats, and bio **Features:** - Authentication required - redirects to /login if not authenticated - Shows user display name (first_name + last_name or email fallback) - Displays join date, location, and description - Statistics: comments count and likes count - "Edit Profile" button visible only for own profile (links to /me) - Responsive layout with avatar placeholder for users without profile images **Comment Integration:** - Updated video comment section to link user avatars to their profiles - Added hover effects on avatars (ring-primary/40 transition) - Username in comments now clickable and links to `/users/[id]` **Translations:** - Added `profile` section to en.ts locales with: - member_since: "Member since {date}" - comments: "Comments" - likes: "Likes" - edit: "Edit Profile" - activity: "Activity" **Design:** - Simplified layout (no cover banner) compared to model profiles - Peony background with card-based UI - Primary color theme with gradient accents - Consistent with existing site design patterns This creates a clear distinction between: - Model profiles (`/models/[slug]`) - public, content-focused - User profiles (`/users/[id]`) - authenticated only, viewer-focused 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/frontend/src/lib/i18n/locales/en.ts | 7 + .../src/routes/users/[id]/+page.server.ts | 45 ++++++ .../src/routes/users/[id]/+page.svelte | 141 ++++++++++++++++++ .../src/routes/videos/[slug]/+page.svelte | 36 ++--- 4 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 packages/frontend/src/routes/users/[id]/+page.server.ts create mode 100644 packages/frontend/src/routes/users/[id]/+page.svelte diff --git a/packages/frontend/src/lib/i18n/locales/en.ts b/packages/frontend/src/lib/i18n/locales/en.ts index 356d0ee..da393d4 100644 --- a/packages/frontend/src/lib/i18n/locales/en.ts +++ b/packages/frontend/src/lib/i18n/locales/en.ts @@ -223,6 +223,13 @@ export default { toast_reset: "Your password has been reset!", }, }, + profile: { + member_since: "Member since {date}", + comments: "Comments", + likes: "Likes", + edit: "Edit Profile", + activity: "Activity", + }, models: { title: "Our Models", description: diff --git a/packages/frontend/src/routes/users/[id]/+page.server.ts b/packages/frontend/src/routes/users/[id]/+page.server.ts new file mode 100644 index 0000000..a63eb23 --- /dev/null +++ b/packages/frontend/src/routes/users/[id]/+page.server.ts @@ -0,0 +1,45 @@ +import { redirect } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ params, locals, fetch }) => { + // Guard: Redirect to login if not authenticated + if (!locals.authStatus.authenticated) { + throw redirect(302, "/login"); + } + + const { id } = params; + + try { + // Fetch user profile data from Directus + const userResponse = await fetch(`/api/users/${id}?fields=id,first_name,last_name,email,description,avatar,date_created,location`); + + if (!userResponse.ok) { + throw redirect(404, "/"); + } + + const userData = await userResponse.json(); + const user = userData.data; + + // Fetch user's comments count + const commentsResponse = await fetch(`/api/comments?filter[user_created][_eq]=${id}&aggregate[count]=*`); + const commentsData = await commentsResponse.json(); + const commentsCount = commentsData.data?.[0]?.count || 0; + + // Fetch user's video likes count + const likesResponse = await fetch(`/api/items/sexy_video_likes?filter[user_id][_eq]=${id}&aggregate[count]=*`); + const likesData = await likesResponse.json(); + const likesCount = likesData.data?.[0]?.count || 0; + + return { + user, + stats: { + comments_count: commentsCount, + likes_count: likesCount, + }, + isOwnProfile: locals.authStatus.user?.id === id, + }; + } catch (error) { + console.error("Failed to load user profile:", error); + throw redirect(404, "/"); + } +}; diff --git a/packages/frontend/src/routes/users/[id]/+page.svelte b/packages/frontend/src/routes/users/[id]/+page.svelte new file mode 100644 index 0000000..d3476dd --- /dev/null +++ b/packages/frontend/src/routes/users/[id]/+page.svelte @@ -0,0 +1,141 @@ + + + + +
+ + +
+ + + + +
+ + + {#if data.isOwnProfile} + + {/if} +
+ + +
+ +
+ {#if data.user.avatar} + {displayName} + {:else} +
+ + {displayName.charAt(0).toUpperCase()} + +
+ {/if} +
+ + +
+

{displayName}

+ +
+ + {$_("profile.member_since", { + values: { date: joinDate }, + })} +
+ + {#if data.user.location} +
+ + {data.user.location} +
+ {/if} + + {#if data.user.description} +

+ {data.user.description} +

+ {/if} + + +
+
+
+ {data.stats.comments_count} +
+
+ {$_("profile.comments")} +
+
+
+
+ {data.stats.likes_count} +
+
+ {$_("profile.likes")} +
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/src/routes/videos/[slug]/+page.svelte b/packages/frontend/src/routes/videos/[slug]/+page.svelte index 2034e9c..b7a5a11 100644 --- a/packages/frontend/src/routes/videos/[slug]/+page.svelte +++ b/packages/frontend/src/routes/videos/[slug]/+page.svelte @@ -442,26 +442,28 @@ let showPlayer = $state(false);
{#each data.comments as comment}
- - - + - {getUserInitials(data.authStatus.user!.artist_name)} - - + + + {getUserInitials(comment.user_created.artist_name)} + + +
- {comment.user_created.artist_name}{comment.user_created.artist_name} {timeAgo.format(