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
18 changes: 9 additions & 9 deletions frontend/components/landing/ui/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onSelectPlatform, onTr
</div>

{/* ========== HERO SECTION ========== */}
<section className="relative z-10 min-h-screen flex flex-col pt-2 pb-32">
<section className="relative z-10 flex flex-col pt-2 mb-30 sm:mb-55">
<div className="max-w-6xl mx-auto w-full">
{/* Hero Content */}
<div className="text-center max-w-5xl mx-auto">
Expand Down Expand Up @@ -185,7 +185,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onSelectPlatform, onTr
<div className="mb-8">
<button
onClick={onTryDemo}
className={`group cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold h-11 px-8 border transition-all duration-200 ${isLight ? 'border-emerald-300/40 text-slate-600 hover:border-emerald-600 hover:text-emerald-600' : 'border-emerald-400/30 text-slate-400 hover:border-emerald-400 hover:text-emerald-300'}`}
className={`group cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-semibold h-11 px-8 border transition-all duration-200 ${isLight ? 'bg-emerald-50/40 hover:bg-emerald-100/50 border-emerald-300/50 text-slate-600 hover:border-emerald-600 hover:text-emerald-600' : 'bg-emerald-950/30 hover:bg-emerald-950/50 border-emerald-400/30 text-slate-400 hover:border-emerald-400 hover:text-emerald-300'}`}
>
<span>Try it with sample data</span>
</button>
Expand All @@ -195,6 +195,13 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onSelectPlatform, onTr
</div>
</section>

{/* ========== REVIEWS SECTION ========== */}
<section id="reviews" className="relative z-10 py-0 sm:py-0">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<ReviewsCarousel />
</div>
</section>

{/* ========== WHY LIFTSHIFT SECTION ========== */}
<section id="why-liftshift" className="relative z-10 px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<div className="max-w-6xl mx-auto">
Expand Down Expand Up @@ -323,13 +330,6 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onSelectPlatform, onTr
</div>
</section>

{/* ========== REVIEWS SECTION ========== */}
<section id="reviews" className="relative z-10 px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
<div className="max-w-6xl mx-auto">
<ReviewsCarousel />
</div>
</section>

{/* ========== PLATFORM DOCK ========== */}
<PlatformDock items={platformDockItems} />

Expand Down
175 changes: 175 additions & 0 deletions frontend/components/landing/ui/RedditCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React, { useMemo, useEffect, useRef } from 'react';
import { ArrowBigUp, Reply, Share2, Award } from 'lucide-react';

const AVATAR_COLORS = [
'#FF4500', '#0079D3', '#46D160', '#DDBD37', '#FF585B',
'#7193FF', '#0DD3BB', '#FF8717', '#A06EE1', '#E063B6',
];

const SUBREDDITS = ['Hevy', 'Strong', 'Lyfta', 'Motra', 'fitness', 'workout', 'Gym', 'bodybuilding', 'strength_training', 'powerlifting'] as const;
const TIMES = ['1h', '2h', '3h', '4h', '5h', '6h', '8h', '10h', '12h', '14h', '16h', '1d', '2d', '3d'];

function hashString(s: string): number {
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i);
return Math.abs(h);
}

export function getColor(username: string): string {
return AVATAR_COLORS[hashString(username) % AVATAR_COLORS.length];
}

export function getUpvotes(username: string): number {
return 2 + (hashString(username + 'up') % 9);
}

export function getSubreddit(username: string, quote: string): string {
const lower = `${username} ${quote}`.toLowerCase();
if (lower.includes('hevy')) return 'Hevy';
if (lower.includes('strong')) return 'Strong';
if (lower.includes('lyfta')) return 'Lyfta';
if (lower.includes('motra')) return 'Motra';
if (lower.includes('bodybuilding') || lower.includes('muscle')) return 'bodybuilding';
if (lower.includes('powerlifting') || lower.includes('pr ') || lower.includes('bench')) return 'powerlifting';
if (lower.includes('gym') || lower.includes('workout')) return 'Gym';
return SUBREDDITS[hashString(username) % SUBREDDITS.length];
}

export function getTimeAgo(username: string): string {
return TIMES[hashString(username + 'time') % TIMES.length];
}

export function SnooAvatar({ color, size = 16 }: { color: string; size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className="shrink-0">
<ellipse cx="12" cy="1.5" rx="1.5" ry="1.5" fill={color} />
<line x1="12" y1="3" x2="12" y2="5.5" stroke={color} strokeWidth="1.5" strokeLinecap="round" />
<ellipse cx="12" cy="15" rx="10" ry="8" fill={color} />
<circle cx="8.5" cy="13.5" r="1.8" fill="#fff" />
<circle cx="15.5" cy="13.5" r="1.8" fill="#fff" />
<path d="M8.5 17 Q12 20 15.5 17" fill="none" stroke="#fff" strokeWidth="1" strokeLinecap="round" />
</svg>
);
}

// ── Shared card face styling ──
export function cardFaceClass(isLight: boolean) {
return isLight
? 'bg-white/35'
: 'bg-black/35';
}

interface RedditCardProps {
username: string;
quote: string;
src: string;
isLight: boolean;
cardId: string;
isFlipped: boolean;
onFlip: () => void;
}

export const RedditCard: React.FC<RedditCardProps> = React.memo(({ username, quote, src, isLight, cardId, isFlipped, onFlip }) => {
const upvotes = useMemo(() => getUpvotes(username), [username]);
const subreddit = useMemo(() => getSubreddit(username, quote), [username, quote]);
const color = useMemo(() => getColor(username), [username]);
const timeAgo = useMemo(() => getTimeAgo(username), [username]);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const startFlipTimer = () => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => onFlip(), 1000);
};

const clearFlipTimer = () => {
if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
};

useEffect(() => {
if (!isFlipped) clearFlipTimer();
return clearFlipTimer;
}, [isFlipped]);

return (
<div
className="w-[300px] sm:w-[340px] h-[160px] sm:h-[140px] cursor-pointer select-none"
style={{ perspective: '800px' }}
onClick={onFlip}
onMouseEnter={() => { if (isFlipped) clearFlipTimer(); }}
onMouseLeave={() => { if (isFlipped) startFlipTimer(); }}
>
Comment on lines +94 to +100
<div
className="relative w-full h-full"
style={{
transformStyle: 'preserve-3d',
transition: 'transform 0.5s ease',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
}}
>
{/* ── Front face: Reddit comment card ── */}
<div
className={`absolute inset-0 rounded-xl overflow-hidden flex flex-col px-3.5 py-3 gap-2 ${cardFaceClass(isLight)}`}
style={{ backfaceVisibility: 'hidden' }}
>
<div className="flex items-center gap-1.5 text-[11px] sm:text-xs">
<SnooAvatar color={color} size={16} />
<span className={`font-medium ${isLight ? 'text-black/80' : 'text-neutral-200'}`}>
r/{subreddit}
</span>
<span className={isLight ? 'text-slate-400' : 'text-neutral-600'}>·</span>
<span className={isLight ? 'text-slate-500' : 'text-neutral-500'}>u/{username}</span>
<span className={isLight ? 'text-slate-400' : 'text-neutral-600'}>·</span>
<span className={isLight ? 'text-slate-400' : 'text-neutral-500'}>{timeAgo}</span>
</div>

<p className={`flex-1 text-[13px] sm:text-sm leading-relaxed line-clamp-3 ${
isLight ? 'text-slate-800' : 'text-neutral-300'
}`}>
{quote.split(' / ').map((part, i) => (
<React.Fragment key={i}>
{i > 0 && <br />}
{part}
</React.Fragment>
))}
</p>

<div className="flex items-center gap-1.5 text-[11px] sm:text-xs">
<ArrowBigUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-[#FF4500]" fill="#FF4500" />
<span className={`font-bold tabular-nums -ml-0.5 ${isLight ? 'text-slate-700' : 'text-neutral-400'}`}>{upvotes}</span>
<span className={`ml-auto flex items-center gap-1 ${isLight ? 'text-slate-400' : 'text-neutral-600'}`}>
<Reply className="w-3 h-3 sm:w-3.5 sm:h-3.5" />Reply
</span>
<span className={`flex items-center gap-1 ${isLight ? 'text-slate-400' : 'text-neutral-600'}`}>
<Share2 className="w-3 h-3 sm:w-3.5 sm:h-3.5" />Share
</span>
<span className={`flex items-center gap-1 ${isLight ? 'text-slate-400' : 'text-neutral-600'}`}>
<Award className="w-3 h-3 sm:w-3.5 sm:h-3.5" />Award
</span>
</div>
</div>

{/* ── Back face: screenshot ── */}
<div
className={`absolute inset-0 rounded-xl overflow-hidden ${cardFaceClass(isLight)}`}
style={{
backfaceVisibility: 'hidden',
transform: 'rotateY(180deg)',
}}
>
<img
src={src}
alt={`Screenshot of ${username}'s Reddit comment`}
className="w-full h-full object-contain p-2"
draggable={false}
loading="lazy"
decoding="async"
/>
</div>
</div>
</div>
);
});

RedditCard.displayName = 'RedditCard';

export default RedditCard;
Loading