diff --git a/attached_assets/CleanShot_2025-12-23_at_22.01.28@2x_1766480562002.png b/attached_assets/CleanShot_2025-12-23_at_22.01.28@2x_1766480562002.png new file mode 100644 index 0000000..df00ff7 Binary files /dev/null and b/attached_assets/CleanShot_2025-12-23_at_22.01.28@2x_1766480562002.png differ diff --git a/client/src/components/ideas-list.tsx b/client/src/components/ideas-list.tsx new file mode 100644 index 0000000..4ca758a --- /dev/null +++ b/client/src/components/ideas-list.tsx @@ -0,0 +1,131 @@ +import { motion } from "framer-motion"; +import { TrendingUp, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { IdeaResponse } from "@shared/schema"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + +interface IdeasListProps { + ideas: IdeaResponse[]; + onVote: (ideaId: number) => void; + isVoting: { [key: number]: boolean }; + votedIdeas: Set; + user: any; + startRank?: number; +} + +export function IdeasList({ + ideas, + onVote, + isVoting, + votedIdeas, + user, + startRank = 4 +}: IdeasListProps) { + const { t } = useTranslation(); + + if (ideas.length === 0) { + return null; + } + + const handleVoteClick = (ideaId: number) => { + if (!user) { + localStorage.setItem("redirectAfterAuth", window.location.pathname); + window.location.href = "/auth"; + return; + } + + if (!votedIdeas.has(ideaId) && !isVoting[ideaId]) { + onVote(ideaId); + } + }; + + const getBorderAccent = (rank: number) => { + if (rank <= 10) return "border-l-amber-400"; + if (rank <= 20) return "border-l-blue-400"; + return "border-l-gray-300 dark:border-l-gray-600"; + }; + + return ( + +

+ {t("ideas.allIdeas", "Todas las Ideas")} +

+ +
+ {ideas.map((idea, index) => { + const rank = startRank + index; + const hasVoted = votedIdeas.has(idea.id); + const voting = isVoting[idea.id]; + + return ( + +
+
+ #{rank} +
+ +
+

+ {idea.title} +

+

+ {idea.description} +

+
+ +
+
+ + {idea.votes} +
+ + +
+
+
+ ); + })} +
+
+ ); +} diff --git a/client/src/components/top3-cards.tsx b/client/src/components/top3-cards.tsx new file mode 100644 index 0000000..81cb049 --- /dev/null +++ b/client/src/components/top3-cards.tsx @@ -0,0 +1,165 @@ +import { motion } from "framer-motion"; +import { Trophy, TrendingUp } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { IdeaResponse } from "@shared/schema"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + +interface Top3CardsProps { + ideas: IdeaResponse[]; + onVote: (ideaId: number) => void; + isVoting: { [key: number]: boolean }; + votedIdeas: Set; + user: any; +} + +export function Top3Cards({ + ideas, + onVote, + isVoting, + votedIdeas, + user +}: Top3CardsProps) { + const { t } = useTranslation(); + + const top3Ideas = ideas.slice(0, 3); + + if (top3Ideas.length === 0) { + return null; + } + + const getBorderColor = (rank: number) => { + switch(rank) { + case 1: return "from-yellow-400 via-amber-400 to-yellow-500"; + case 2: return "from-blue-400 via-cyan-400 to-blue-500"; + case 3: return "from-orange-400 via-amber-500 to-orange-500"; + default: return "from-gray-300 to-gray-400"; + } + }; + + const getBadgeColor = (rank: number) => { + switch(rank) { + case 1: return "bg-gradient-to-r from-yellow-400 to-amber-500 text-yellow-900"; + case 2: return "bg-gradient-to-r from-blue-400 to-cyan-500 text-blue-900"; + case 3: return "bg-gradient-to-r from-orange-400 to-amber-500 text-orange-900"; + default: return "bg-gray-200 text-gray-700"; + } + }; + + const handleVoteClick = (ideaId: number) => { + if (!user) { + localStorage.setItem("redirectAfterAuth", window.location.pathname); + window.location.href = "/auth"; + return; + } + + if (!votedIdeas.has(ideaId) && !isVoting[ideaId]) { + onVote(ideaId); + } + }; + + return ( + +
+ +

+ Top 3 Ideas +

+
+ +
+ {top3Ideas.map((idea, index) => { + const rank = index + 1; + const hasVoted = votedIdeas.has(idea.id); + const voting = isVoting[idea.id]; + + return ( + +
+
+
+ +
+
+
+ + #{rank} +
+ +
+ + {idea.votes} +
+
+ +
+

+ {idea.title} +

+

+ {idea.description} +

+
+ +
+ {idea.suggestedBy && ( + + {t("ideas.by", "Por")} {idea.suggestedBy} + + )} + +
+
+ + ); + })} +
+
+ ); +} diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index b934146..b5a44f9 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -399,7 +399,10 @@ "loadError": "Failed to load ideas", "analyzed": "Analyzed", "activeIdeas": "Active Ideas", - "totalVotes": "Total Votes" + "totalVotes": "Total Votes", + "allIdeas": "All Ideas", + "by": "By", + "empty": "No ideas yet" }, "priority": { "hybridScore": "Priority score (combines votes + YouTube opportunity)", @@ -897,7 +900,9 @@ "watchDemo": "▶ Watch Demo" }, "suggest": { - "idea": "Suggest Idea" + "idea": "Suggest Idea", + "notFound": "Didn't find what you were looking for?", + "helpCreator": "Help {{creator}} by suggesting an idea" }, "redemptions": { "short": "Redemptions", diff --git a/client/src/locales/es/translation.json b/client/src/locales/es/translation.json index fde8814..7609c5e 100644 --- a/client/src/locales/es/translation.json +++ b/client/src/locales/es/translation.json @@ -401,7 +401,10 @@ "loadError": "Error al cargar ideas", "analyzed": "Analizado", "activeIdeas": "Ideas Activas", - "totalVotes": "Votos Totales" + "totalVotes": "Votos Totales", + "allIdeas": "Todas las Ideas", + "by": "Por", + "empty": "Aún no hay ideas" }, "priority": { "hybridScore": "Puntuación de prioridad (combina votos + oportunidad YouTube)", @@ -896,7 +899,9 @@ "watchDemo": "▶ Ver Demo" }, "suggest": { - "idea": "Sugerir Idea" + "idea": "Sugerir Idea", + "notFound": "¿No encontraste lo que buscabas?", + "helpCreator": "Ayuda a {{creator}} sugiriendo una idea" }, "errors": { "notEnoughPoints": "No tienes suficientes puntos para enviar una sugerencia", diff --git a/client/src/pages/modern-public-profile.tsx b/client/src/pages/modern-public-profile.tsx index 7971174..49e2d14 100644 --- a/client/src/pages/modern-public-profile.tsx +++ b/client/src/pages/modern-public-profile.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { useParams, useLocation } from "wouter"; import { useQuery } from "@tanstack/react-query"; import { motion, AnimatePresence } from "framer-motion"; @@ -9,30 +9,27 @@ import { IdeaResponse } from "@shared/schema"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { Loader2, - Share2, ExternalLink, Twitter, Instagram, Youtube, Globe, - UserPlus, ArrowLeft, + Lightbulb, + Coins, + UserPlus, } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { ThemeToggle } from "@/components/theme-toggle"; -import { LanguageToggle } from "@/components/language-toggle"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { ModernSidebar } from "@/components/modern-sidebar"; import { MobileBottomNav } from "@/components/mobile-bottom-nav"; -import { CompactIdeaCard } from "@/components/compact-idea-card"; import SuggestIdeaModal from "@/components/suggest-idea-modal"; import AudienceStats from "@/components/audience-stats"; import { PublicStore } from "@/components/public-store"; -import { UserIndicator } from "@/components/user-indicator"; import { MobileMenu } from "@/components/mobile-menu"; -import { Top3Podium } from "@/components/top3-podium"; +import { Top3Cards } from "@/components/top3-cards"; +import { IdeasList } from "@/components/ideas-list"; import { LeaderboardSkeleton } from "@/components/leaderboard-skeleton"; import { FaTiktok } from "react-icons/fa"; import { FaThreads } from "react-icons/fa6"; @@ -72,10 +69,8 @@ export default function ModernPublicProfile() { enabled: !!username, }); - // Check if current user is the profile owner const isOwnProfile = user?.username === username; - // Query user points for this specific creator const { data: userPoints } = useQuery<{ totalPoints: number }>({ queryKey: [`/api/user/points/${data?.creator?.id}`], enabled: !!user && !isOwnProfile && !!data?.creator?.id, @@ -93,7 +88,6 @@ export default function ModernPublicProfile() { } }, [error, navigate, toast]); - // Load voted ideas from localStorage only for authenticated users useEffect(() => { if (user) { const userKey = `votedIdeas_${user.id}`; @@ -106,7 +100,6 @@ export default function ModernPublicProfile() { } }, [user]); - // Check vote status for all ideas when data loads useEffect(() => { if (!data?.ideas || !user) { setVotedIdeas(new Set()); @@ -155,7 +148,6 @@ export default function ModernPublicProfile() { method: "POST", }); - // Update localStorage using the fresh state to avoid race condition const userKey = `votedIdeas_${user.id}`; const updatedVotedIdeas = Array.from(new Set([...Array.from(votedIdeas), ideaId])); localStorage.setItem(userKey, JSON.stringify(updatedVotedIdeas)); @@ -167,7 +159,6 @@ export default function ModernPublicProfile() { description: t("vote.successDesc"), }); - // Invalidate points for this specific creator if (data?.creator?.id) { queryClient.invalidateQueries({ queryKey: [`/api/user/points/${data.creator.id}`] }); } @@ -216,7 +207,7 @@ export default function ModernPublicProfile() { if (isLoading) { return ( -
+
); @@ -224,7 +215,7 @@ export default function ModernPublicProfile() { if (!data) { return ( -
+

{t("profile.notFound")} @@ -248,215 +239,130 @@ export default function ModernPublicProfile() { { platform: "website", url: creator.websiteUrl }, ].filter(link => link.url); - const renderProfileSection = () => ( - - {/* Creator Info Card */} -
-
-
- {/* Avatar */} - - - - - {creator.username.charAt(0).toUpperCase()} - - - - - {/* Info */} -
-

- {creator.username} -

- {creator.profileDescription && ( -

- {creator.profileDescription} -

- )} - - {/* Social Links */} - {socialLinks.length > 0 && ( -
- {socialLinks.map((link) => ( - - {getSocialIcon(link.platform)} - {link.platform} - - ))} -
- )} -
- - {/* Action Button */} - {!isOwnProfile && user && ( - - - - )} -
-
-
-
- ); - const renderIdeasSection = () => { if (isLoading) { return ; } return ( - - {/* Creator Profile Info */} -
-
-
- {/* Avatar */} - - - - - {creator.username.charAt(0).toUpperCase()} - - - - - {/* Info */} -
-

- {creator.username} -

- {creator.profileDescription && ( -

- {creator.profileDescription} -

- )} - - {/* Social Links */} - {socialLinks.length > 0 && ( -
- {socialLinks.map((link) => ( - - {getSocialIcon(link.platform)} - {link.platform} - - ))} -
- )} -
+ +
+ +
+ + + + + {creator.username.charAt(0).toUpperCase()} + + + -
+
+

+ {creator.username} +

+ {creator.profileDescription && ( +

+ {creator.profileDescription} +

+ )} + + {socialLinks.length > 0 && ( +
+ {socialLinks.map((link) => ( + + {getSocialIcon(link.platform)} + {link.platform} + + ))} +
+ )} +
+
+
-
- {/* Ideas Section Header */} -
-

- {t("ideas.title")} -

- - {ideas.length} {t("ideas.total")} - -
+ {ideas.length === 0 ? ( +
+ +

+ {t("ideas.empty")} +

+
+ ) : ( + <> + - {ideas.length === 0 ? ( -
-

- {t("ideas.empty")} -

-
- ) : ( - <> - {/* Top 3 Podium */} - {ideas.length > 0 && ( -
- 3 && ( + -
- )} - - {/* Other Ideas */} - {ideas.length > 3 && ( - -
-

- {t("leaderboard.otherIdeas")} + )} + + {/* CTA to suggest idea at the end of the list */} + {user && !isOwnProfile && ( + + +

+ {t("suggest.notFound", "¿No encontraste lo que buscabas?")}

-

- {t("leaderboard.otherIdeasDesc")} +

+ {t("suggest.helpCreator", "Ayuda a {{creator}} sugiriendo una idea", { creator: creator.username })}

-

- -
- {ideas.slice(3).map((idea, index) => ( - - ))} -
-
- )} - - )} - + + + )} + + )} + ); }; @@ -491,22 +397,32 @@ export default function ModernPublicProfile() { }; return ( -
- {/* Desktop User Indicator */} -
- -
+
+ {user && !isOwnProfile && ( +
+ + + {userPoints?.totalPoints ?? 0} + +
+ )} - {/* Mobile Header */} -
-
-
+
+
+

Fanlist

-
- {user && } +
+ {user && !isOwnProfile && ( +
+ + + {userPoints?.totalPoints ?? 0} + +
+ )}
- - {/* Main Layout */} -
- {/* Desktop Sidebar */} +
- {/* Main Content */}
-
+
- {/* Mobile Bottom Navigation */}
- {/* Suggest Idea Modal */}
); -} \ No newline at end of file +} diff --git a/replit.md b/replit.md index ca9185e..059fedd 100644 --- a/replit.md +++ b/replit.md @@ -5,6 +5,17 @@ Fanlist is a web application designed to empower content creators by enabling th ## Recent Changes +### December 23, 2025 - Public Profile Redesign +- **New Layout**: Redesigned public profile page with modern card-based layout inspired by reference design. +- **Greeting Header**: Added personalized "Hello/Hola USERNAME" greeting at the top of the page. +- **Top3Cards Component**: New component displaying top 3 ideas with gradient borders (gold for #1, blue for #2, orange for #3). +- **IdeasList Component**: Clean list view for remaining ideas with left border accent and minimal styling. +- **Outline Vote Buttons**: Vote buttons now use outline style with dark borders instead of filled backgrounds. +- **Creator Info Card**: Reorganized creator section with avatar, name, description, and social links in a clean card. +- **Theme Support**: Full dark/light theme support with bg-gray-50/bg-gray-950 backgrounds. +- **Internationalization**: Added translations for "allIdeas", "by", and "empty" in both English and Spanish. +- **Responsive Design**: Layout adapts well to mobile and desktop with consistent styling. + ### November 7, 2025 - Idea Completion and Archival System - **Dual-Action Management**: Implemented both "Complete" and "Delete" actions for ideas, allowing creators to archive finished ideas separately from permanent deletion. - **Schema Enhancement**: Added 'completed' status to ideas alongside existing 'approved' and 'pending' statuses.