Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions frontend/components/profile/AchievementPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import Image from "next/image";
import { AchievementCard } from "./AchievementCard";
import type { Achievement } from "./UserProfileView";

interface AchievementPreviewProps {
achievements: Achievement[];
onViewAllAchievements: () => void;
}

export function AchievementPreview({
achievements,
onViewAllAchievements,
}: AchievementPreviewProps) {
const visibleAchievements = achievements.slice(0, 3);

return (
<section className="w-full">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-foreground">Achievements</h3>
<button
type="button"
onClick={onViewAllAchievements}
className="text-sm text-[#3B82F6] cursor-pointer transition-transform duration-200 hover:scale-105"
>
View All
</button>
</div>

<div className="flex gap-4 overflow-x-auto pb-2 lg:justify-start">
{visibleAchievements.map((achievement) => (
<AchievementCard
key={achievement.id}
icon={
<Image
src={achievement.icon}
alt={achievement.title}
width={48}
height={48}
className="h-12 w-12"
loading="lazy"
/>
}
title={achievement.title}
date={achievement.date}
badge={achievement.value}
/>
))}
</div>
</section>
);
}

49 changes: 49 additions & 0 deletions frontend/components/profile/FollowButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";

import { UserCheck, UserPlus } from "lucide-react";
import Button from "../Button";

interface FollowButtonProps {
isFollowing: boolean;
onFollow: () => void;
onUnfollow: () => void;
}

export function FollowButton({
isFollowing,
onFollow,
onUnfollow,
}: FollowButtonProps) {
const handleClick = () => {
if (isFollowing) {
onUnfollow();
} else {
onFollow();
}
};

const Icon = isFollowing ? UserCheck : UserPlus;

if (isFollowing) {
return (
<button
type="button"
onClick={handleClick}
className="flex flex-1 min-w-0 items-center justify-center gap-2 rounded-xl border border-gray-800 bg-transparent py-3 px-6 text-sm font-medium text-[#8B5CF6] transition-colors hover:border-gray-700"
>
<Icon className="h-4 w-4" />
<span>Following</span>
</button>
);
}

return (
<Button type="button" variant="primary" onClick={handleClick} className="flex-1 min-w-0">
<span className="flex items-center justify-center gap-2 text-sm font-medium">
<UserPlus className="h-4 w-4" />
<span>Follow</span>
</span>
</Button>
);
}

4 changes: 4 additions & 0 deletions frontend/components/profile/OverviewCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { StatCard } from "./StatCard";

export { StatCard as OverviewCard };

4 changes: 4 additions & 0 deletions frontend/components/profile/OverviewGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ProfileOverview } from "./ProfileOverview";

export { ProfileOverview as OverviewGrid };

37 changes: 37 additions & 0 deletions frontend/components/profile/ProfileStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

interface ProfileStatsProps {
followingCount: number;
followersCount: number;
onFollowingClick: () => void;
onFollowersClick: () => void;
}

export function ProfileStats({
followingCount,
followersCount,
onFollowingClick,
onFollowersClick,
}: ProfileStatsProps) {
return (
<div className="mt-4 flex items-center justify-center gap-4 text-sm text-[#3B82F6]">
<button
type="button"
onClick={onFollowingClick}
className="cursor-pointer transition-colors hover:text-blue-300"
>
<span className="font-semibold">{followingCount}</span>{" "}
<span className="text-xs sm:text-sm">Following</span>
</button>
<button
type="button"
onClick={onFollowersClick}
className="cursor-pointer transition-colors hover:text-blue-300"
>
<span className="font-semibold">{followersCount}</span>{" "}
<span className="text-xs sm:text-sm">Followers</span>
</button>
</div>
);
}

151 changes: 151 additions & 0 deletions frontend/components/profile/UserProfileView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"use client";

import { X, Share2 } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@radix-ui/react-avatar";
import { FollowButton } from "./FollowButton";
import { ProfileStats } from "./ProfileStats";
import { OverviewGrid } from "./OverviewGrid";
import { AchievementPreview } from "./AchievementPreview";

export interface Achievement {
id: string;
icon: string;
title: string;
value: string;
date: string;
}

export interface UserProfileViewProps {
id: string;
username: string;
handle: string;
avatar: string;
joinedDate: string;
followingCount: number;
followersCount: number;
isFollowing: boolean;
dayStreak: number;
totalPoints: number;
rank: number;
challengeLevel: string;
achievements: Achievement[];
onFollow: () => void;
onUnfollow: () => void;
onShare: () => void;
onFollowingClick: () => void;
onFollowersClick: () => void;
onClose: () => void;
onViewAllAchievements: () => void;
}

export function UserProfileView(props: UserProfileViewProps) {
const {
username,
handle,
avatar,
joinedDate,
followingCount,
followersCount,
isFollowing,
dayStreak,
totalPoints,
rank,
challengeLevel,
achievements,
onFollow,
onUnfollow,
onShare,
onFollowingClick,
onFollowersClick,
onClose,
onViewAllAchievements,
} = props;

const initials = username
.split(" ")
.map((n) => n[0])
.join("");

return (
<div className="flex h-full w-full items-center justify-center bg-transparent text-[#E6E6E6]">
<div className="relative w-full max-w-lg rounded-3xl bg-transparent px-6 py-6 shadow-xl sm:px-8 sm:py-8">
{/* Close button */}
<button
type="button"
onClick={onClose}
aria-label="Close profile"
className="absolute left-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border border-gray-800 bg-[#050C16] text-gray-300 hover:bg-gray-800">
<X className="h-4 w-4" />
</button>

<div className="mt-4 flex flex-col items-center">
{/* Avatar & basic info */}
<div className="flex flex-col items-center gap-1">
<Avatar className="h-24 w-24 sm:h-28 sm:w-28">
<AvatarImage src={avatar || "/profileAvatar.svg"} alt={username} />
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-indigo-600 text-xl font-semibold">
{initials}
</AvatarFallback>
</Avatar>

<div className="flex flex-col items-center gap-1 text-center">
<h2 className="text-xl font-semibold text-foreground">
{username}
</h2>
<p className="text-sm text-muted-foreground">@{handle}</p>
<p className="text-sm text-muted-foreground">
Joined {joinedDate}
</p>
</div>
</div>

{/* Stats row */}
<ProfileStats
followingCount={followingCount}
followersCount={followersCount}
onFollowingClick={onFollowingClick}
onFollowersClick={onFollowersClick}
/>

{/* Action bar */}
<div className="mt-4 flex w-full items-center gap-3">
<FollowButton
isFollowing={isFollowing}
onFollow={onFollow}
onUnfollow={onUnfollow}
/>
<button
type="button"
onClick={onShare}
aria-label="Share profile"
className="flex h-11 w-11 items-center justify-center rounded-xl border border-gray-800 bg-[#050C16] text-gray-200 transition-colors hover:border-[#3B82F6] hover:text-[#3B82F6]">
<Share2 className="h-4 w-4" />
</button>
</div>

<div className="flex flex-col gap-10 w-full">
{/* Overview grid */}

<OverviewGrid
dayStreak={dayStreak}
totalPoints={totalPoints}
rank={rank}
challengeLevel={challengeLevel}
/>

{/* Achievements preview */}
<AchievementPreview
achievements={achievements}
onViewAllAchievements={onViewAllAchievements}
/>
</div>
</div>
</div>
</div>
);
}