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

<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>