Add a super like button and download links for Linux distros

Implement a "super like" functionality with a dedicated button and associated UI elements. Add download URLs to distro data and display them on the results screen with download links and visual indicators for super liked items. Add keyboard support for left, right, and up arrow keys to control swiping.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e522d728-b28a-437f-b576-83935afe7ea0
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 393186dc-5528-4239-9955-fe74ab3cb710
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/55e426a8-5a2b-421b-8bd4-30481ae0063b/e522d728-b28a-437f-b576-83935afe7ea0/LaBrqqz
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
yusufipk
2025-12-17 13:42:47 +00:00
parent 59f1f2bd99
commit 2d26d5d4f9
5 changed files with 60 additions and 9 deletions
+4
View File
@@ -4,6 +4,7 @@ import { Heart, X, RotateCcw, Zap } from 'lucide-react';
interface ActionButtonsProps { interface ActionButtonsProps {
onLike: () => void; onLike: () => void;
onDislike: () => void; onDislike: () => void;
onSuperLike?: () => void;
onReset?: () => void; onReset?: () => void;
showReset?: boolean; showReset?: boolean;
disabled?: boolean; disabled?: boolean;
@@ -12,6 +13,7 @@ interface ActionButtonsProps {
export default function ActionButtons({ export default function ActionButtons({
onLike, onLike,
onDislike, onDislike,
onSuperLike,
onReset, onReset,
showReset = false, showReset = false,
disabled = false, disabled = false,
@@ -69,6 +71,8 @@ export default function ActionButtons({
<motion.button <motion.button
className="group relative flex h-14 w-14 items-center justify-center rounded-full" className="group relative flex h-14 w-14 items-center justify-center rounded-full"
onClick={onSuperLike}
disabled={disabled}
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
data-testid="button-superlike" data-testid="button-superlike"
+26 -3
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import SwipeCard from './SwipeCard'; import SwipeCard from './SwipeCard';
import ActionButtons from './ActionButtons'; import ActionButtons from './ActionButtons';
@@ -13,19 +13,24 @@ interface CardStackProps {
export default function CardStack({ cards, onBackgroundChange }: CardStackProps) { export default function CardStack({ cards, onBackgroundChange }: CardStackProps) {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [likedCards, setLikedCards] = useState<DistroCard[]>([]); const [likedCards, setLikedCards] = useState<DistroCard[]>([]);
const [superLikedCards, setSuperLikedCards] = useState<DistroCard[]>([]);
const [dislikedCards, setDislikedCards] = useState<DistroCard[]>([]); const [dislikedCards, setDislikedCards] = useState<DistroCard[]>([]);
const visibleCards = cards.slice(currentIndex, currentIndex + 3); const visibleCards = cards.slice(currentIndex, currentIndex + 3);
const isFinished = currentIndex >= cards.length; const isFinished = currentIndex >= cards.length;
const handleSwipe = useCallback( const handleSwipe = useCallback(
(direction: 'left' | 'right') => { (direction: 'left' | 'right' | 'super') => {
const currentCard = cards[currentIndex]; const currentCard = cards[currentIndex];
if (!currentCard) return; if (!currentCard) return;
if (direction === 'right') { if (direction === 'right') {
setLikedCards((prev) => [...prev, currentCard]); setLikedCards((prev) => [...prev, currentCard]);
console.log('Liked:', currentCard.distroName); console.log('Liked:', currentCard.distroName);
} else if (direction === 'super') {
setSuperLikedCards((prev) => [...prev, currentCard]);
setLikedCards((prev) => [...prev, currentCard]);
console.log('Super Liked:', currentCard.distroName);
} else { } else {
setDislikedCards((prev) => [...prev, currentCard]); setDislikedCards((prev) => [...prev, currentCard]);
console.log('Disliked:', currentCard.distroName); console.log('Disliked:', currentCard.distroName);
@@ -42,6 +47,23 @@ export default function CardStack({ cards, onBackgroundChange }: CardStackProps)
[currentIndex, cards, onBackgroundChange] [currentIndex, cards, onBackgroundChange]
); );
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (currentIndex >= cards.length) return;
if (e.key === 'ArrowLeft') {
handleSwipe('left');
} else if (e.key === 'ArrowRight') {
handleSwipe('right');
} else if (e.key === 'ArrowUp') {
handleSwipe('super');
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSwipe, currentIndex, cards.length]);
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
setCurrentIndex(0); setCurrentIndex(0);
setLikedCards([]); setLikedCards([]);
@@ -52,7 +74,7 @@ export default function CardStack({ cards, onBackgroundChange }: CardStackProps)
}, [cards, onBackgroundChange]); }, [cards, onBackgroundChange]);
if (isFinished) { if (isFinished) {
return <EndOfDeck likedCards={likedCards} onReset={handleReset} />; return <EndOfDeck likedCards={likedCards} superLikedCards={superLikedCards} onReset={handleReset} />;
} }
return ( return (
@@ -89,6 +111,7 @@ export default function CardStack({ cards, onBackgroundChange }: CardStackProps)
<ActionButtons <ActionButtons
onLike={() => handleSwipe('right')} onLike={() => handleSwipe('right')}
onDislike={() => handleSwipe('left')} onDislike={() => handleSwipe('left')}
onSuperLike={() => handleSwipe('super')}
onReset={handleReset} onReset={handleReset}
showReset={currentIndex > 0} showReset={currentIndex > 0}
disabled={isFinished} disabled={isFinished}
+21 -6
View File
@@ -1,13 +1,14 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { PartyPopper, RotateCcw, Download, Trophy, Star, ExternalLink } from 'lucide-react'; import { RotateCcw, Download, Trophy, Star, ExternalLink, Zap } from 'lucide-react';
import type { DistroCard } from '@/data/distros'; import type { DistroCard } from '@/data/distros';
interface EndOfDeckProps { interface EndOfDeckProps {
likedCards: DistroCard[]; likedCards: DistroCard[];
superLikedCards: DistroCard[];
onReset: () => void; onReset: () => void;
} }
export default function EndOfDeck({ likedCards, onReset }: EndOfDeckProps) { export default function EndOfDeck({ likedCards, superLikedCards, onReset }: EndOfDeckProps) {
return ( return (
<motion.div <motion.div
className="flex flex-col items-center gap-10 text-center px-4" className="flex flex-col items-center gap-10 text-center px-4"
@@ -73,7 +74,14 @@ export default function EndOfDeck({ likedCards, onReset }: EndOfDeckProps) {
data-testid={`result-card-${card.id}`} data-testid={`result-card-${card.id}`}
> >
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-pink-500/20 backdrop-blur-xl" /> <div className="absolute inset-0 bg-gradient-to-br from-purple-500/20 to-pink-500/20 backdrop-blur-xl" />
<div className="absolute inset-0 rounded-2xl border border-white/10" /> <div className={`absolute inset-0 rounded-2xl border ${superLikedCards.some(s => s.id === card.id) ? 'border-cyan-400/50' : 'border-white/10'}`} />
{superLikedCards.some(s => s.id === card.id) && (
<div className="absolute top-2 left-2 z-20 flex items-center gap-1 rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 px-2 py-1">
<Zap className="h-3 w-3 text-white" fill="currentColor" />
<span className="text-xs font-bold text-white">SUPER</span>
</div>
)}
<div className="relative"> <div className="relative">
<img <img
@@ -83,11 +91,18 @@ export default function EndOfDeck({ likedCards, onReset }: EndOfDeckProps) {
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent" />
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"> <a
<div className="rounded-full bg-white/20 backdrop-blur-md p-2"> href={card.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
data-testid={`link-download-${card.id}`}
>
<div className="rounded-full bg-white/20 backdrop-blur-md p-2 hover:bg-white/30 transition-colors">
<ExternalLink className="h-4 w-4 text-white" /> <ExternalLink className="h-4 w-4 text-white" />
</div> </div>
</div> </a>
</div> </div>
<div className="relative p-4 bg-gradient-to-r from-gray-900/90 to-gray-800/90"> <div className="relative p-4 bg-gradient-to-r from-gray-900/90 to-gray-800/90">
@@ -3,11 +3,13 @@ import { distroCards } from '@/data/distros';
export default function EndOfDeckExample() { export default function EndOfDeckExample() {
const likedCards = distroCards.slice(0, 3); const likedCards = distroCards.slice(0, 3);
const superLikedCards = distroCards.slice(0, 1);
return ( return (
<div className="flex min-h-[500px] items-center justify-center bg-gradient-to-br from-purple-900/50 to-blue-900/50 p-8"> <div className="flex min-h-[500px] items-center justify-center bg-gradient-to-br from-purple-900/50 to-blue-900/50 p-8">
<EndOfDeck <EndOfDeck
likedCards={likedCards} likedCards={likedCards}
superLikedCards={superLikedCards}
onReset={() => console.log('Reset!')} onReset={() => console.log('Reset!')}
/> />
</div> </div>
+7
View File
@@ -13,6 +13,7 @@ export interface DistroCard {
desktopEnvironment: string; desktopEnvironment: string;
tags: string[]; tags: string[];
colorPalette: string[]; colorPalette: string[];
downloadUrl: string;
} }
export const distroCards: DistroCard[] = [ export const distroCards: DistroCard[] = [
@@ -24,6 +25,7 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'GNOME', desktopEnvironment: 'GNOME',
tags: ['beginner-friendly', 'stable', 'popular'], tags: ['beginner-friendly', 'stable', 'popular'],
colorPalette: ['#E95420', '#77216F', '#5E2750'], colorPalette: ['#E95420', '#77216F', '#5E2750'],
downloadUrl: 'https://ubuntu.com/download',
}, },
{ {
id: '2', id: '2',
@@ -33,6 +35,7 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'i3wm', desktopEnvironment: 'i3wm',
tags: ['advanced', 'minimal', 'customizable'], tags: ['advanced', 'minimal', 'customizable'],
colorPalette: ['#1793D1', '#333333', '#0D47A1'], colorPalette: ['#1793D1', '#333333', '#0D47A1'],
downloadUrl: 'https://archlinux.org/download/',
}, },
{ {
id: '3', id: '3',
@@ -42,6 +45,7 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'Pantheon', desktopEnvironment: 'Pantheon',
tags: ['beautiful', 'simple', 'curated'], tags: ['beautiful', 'simple', 'curated'],
colorPalette: ['#64BAFF', '#3689E6', '#0D52BF'], colorPalette: ['#64BAFF', '#3689E6', '#0D52BF'],
downloadUrl: 'https://elementary.io/',
}, },
{ {
id: '4', id: '4',
@@ -51,6 +55,7 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'KDE Plasma', desktopEnvironment: 'KDE Plasma',
tags: ['cutting-edge', 'developer', 'reliable'], tags: ['cutting-edge', 'developer', 'reliable'],
colorPalette: ['#51A2DA', '#294172', '#3C6EB4'], colorPalette: ['#51A2DA', '#294172', '#3C6EB4'],
downloadUrl: 'https://fedoraproject.org/workstation/download',
}, },
{ {
id: '5', id: '5',
@@ -60,6 +65,7 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'Cinnamon', desktopEnvironment: 'Cinnamon',
tags: ['familiar', 'stable', 'beginner-friendly'], tags: ['familiar', 'stable', 'beginner-friendly'],
colorPalette: ['#87CF3E', '#5FAD56', '#3E8E41'], colorPalette: ['#87CF3E', '#5FAD56', '#3E8E41'],
downloadUrl: 'https://linuxmint.com/download.php',
}, },
{ {
id: '6', id: '6',
@@ -69,5 +75,6 @@ export const distroCards: DistroCard[] = [
desktopEnvironment: 'COSMIC', desktopEnvironment: 'COSMIC',
tags: ['gaming', 'developer', 'modern'], tags: ['gaming', 'developer', 'modern'],
colorPalette: ['#FAA41A', '#48B9C7', '#574F4A'], colorPalette: ['#FAA41A', '#48B9C7', '#574F4A'],
downloadUrl: 'https://pop.system76.com/',
}, },
]; ];