You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
135 lines
3.6 KiB
135 lines
3.6 KiB
<script lang="ts"> |
|
import Icon from '../ui/Icon.svelte'; |
|
|
|
interface Props { |
|
upvotes: number; |
|
downvotes: number; |
|
votesCalculated?: boolean; // If false, show "Calculating votes" state |
|
size?: 'xs' | 'sm' | 'base'; // Size variant |
|
userVote?: '+' | '-' | null; // User's current vote |
|
onVote?: (vote: '+' | '-') => void; // Callback when user clicks a vote |
|
isLoggedIn?: boolean; // Whether user is logged in |
|
} |
|
|
|
let { |
|
upvotes = 0, |
|
downvotes = 0, |
|
votesCalculated = true, |
|
size = 'xs', |
|
userVote = null, |
|
onVote, |
|
isLoggedIn = false |
|
}: Props = $props(); |
|
|
|
// Create size class dynamically |
|
let sizeClass = $derived(`text-${size}`); |
|
|
|
function handleVoteClick(vote: '+' | '-') { |
|
if (!isLoggedIn || !onVote) return; |
|
onVote(vote); |
|
} |
|
</script> |
|
|
|
<span class="vote-counts flex items-center gap-2 {sizeClass}" class:calculating={!votesCalculated}> |
|
{#if !votesCalculated} |
|
<span class="flex items-center gap-1 text-fog-text-light dark:text-fog-dark-text-light opacity-50"> |
|
<span class="upvotes flex items-center gap-1"> |
|
<Icon name="chevron-up" size={14} /> |
|
<span>0</span> |
|
</span> |
|
<span class="downvotes flex items-center gap-1"> |
|
<Icon name="chevron-down" size={14} /> |
|
<span>0</span> |
|
</span> |
|
<span class="calculating-label text-xs ml-1">Calculating votes</span> |
|
</span> |
|
{:else} |
|
<span class="flex items-center gap-2 text-fog-text-light dark:text-fog-dark-text-light"> |
|
<button |
|
type="button" |
|
onclick={() => handleVoteClick('+')} |
|
disabled={!isLoggedIn || !onVote} |
|
class="vote-emoji upvotes {userVote === '+' ? 'active' : ''} {!isLoggedIn || !onVote ? 'disabled' : ''}" |
|
class:has-votes={upvotes > 0} |
|
title={isLoggedIn && onVote ? "Upvote" : !isLoggedIn ? "Login to vote" : ""} |
|
aria-label="Upvote" |
|
> |
|
<Icon name="chevron-up" size={14} /> |
|
<span>{upvotes}</span> |
|
</button> |
|
<button |
|
type="button" |
|
onclick={() => handleVoteClick('-')} |
|
disabled={!isLoggedIn || !onVote} |
|
class="vote-emoji downvotes {userVote === '-' ? 'active' : ''} {!isLoggedIn || !onVote ? 'disabled' : ''}" |
|
class:has-votes={downvotes > 0} |
|
title={isLoggedIn && onVote ? "Downvote" : !isLoggedIn ? "Login to vote" : ""} |
|
aria-label="Downvote" |
|
> |
|
<Icon name="chevron-down" size={14} /> |
|
<span>{downvotes}</span> |
|
</button> |
|
</span> |
|
{/if} |
|
</span> |
|
|
|
<style> |
|
.vote-counts { |
|
display: inline-flex; |
|
align-items: center; |
|
} |
|
|
|
.vote-counts.calculating { |
|
opacity: 0.6; |
|
} |
|
|
|
.vote-emoji { |
|
background: transparent; |
|
border: none; |
|
padding: 0.25rem 0.5rem; |
|
cursor: pointer; |
|
transition: all 0.2s ease; |
|
font-size: inherit; |
|
color: inherit; |
|
display: inline-flex; |
|
align-items: center; |
|
gap: 0.25rem; |
|
border-radius: 0.25rem; |
|
opacity: 0.7; |
|
} |
|
|
|
.vote-emoji.disabled { |
|
cursor: not-allowed; |
|
opacity: 0.5; |
|
} |
|
|
|
.vote-emoji:not(.disabled):hover { |
|
background: var(--fog-highlight, #f3f4f6); |
|
opacity: 0.9; |
|
} |
|
|
|
:global(.dark) .vote-emoji:not(.disabled):hover { |
|
background: var(--fog-dark-highlight, #374151); |
|
} |
|
|
|
.vote-emoji.has-votes { |
|
font-weight: 700; |
|
opacity: 1; |
|
} |
|
|
|
.vote-emoji.active { |
|
opacity: 1; |
|
background: var(--fog-accent, #64748b); |
|
color: var(--fog-text, #ffffff); |
|
} |
|
|
|
:global(.dark) .vote-emoji.active { |
|
background: var(--fog-dark-accent, #64748b); |
|
color: var(--fog-dark-text, #ffffff); |
|
} |
|
|
|
.vote-counts .calculating-label { |
|
font-style: italic; |
|
opacity: 0.7; |
|
} |
|
</style>
|
|
|