Skip to content
Closed
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
672 changes: 266 additions & 406 deletions src/lib/components/ContestTable.svelte

Large diffs are not rendered by default.

675 changes: 244 additions & 431 deletions src/lib/components/ProblemTable.svelte

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions src/lib/components/tables/BaseTable.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<script lang="ts">
// Common styling classes
export const tableWrapperClass = 'mt-4 w-full';
export const tableContainerClass = 'table-wrapper rounded-lg bg-[var(--color-secondary)] shadow-sm';
export const tableClass =
'w-full min-w-[900px] table-fixed border-collapse overflow-hidden bg-[var(--color-secondary)]';
export const headerCellClass = 'sticky top-0 z-10 bg-[var(--color-tertiary)] p-3 font-bold';
export const headerCellClickableClass = `${headerCellClass} cursor-pointer transition-colors duration-200 hover:bg-[color-mix(in_oklab,var(--color-tertiary)_90%,var(--color-accent)_10%,transparent)]`;
export const rowClass =
'relative border-b border-[var(--color-border)] transition-colors duration-200 last:border-b-0';
export const solvedRowClass = 'border-l-4 border-l-[rgb(34_197_94)] bg-[var(--color-solved-row)]';
export const unsolvedRowClass = 'hover:bg-black/5';
export const cellClass = 'p-3';
</script>

<div class={tableWrapperClass}>
<div class={tableContainerClass}>
<table class={tableClass}>
<thead>
<tr>
<!-- Header slot -->
<slot name="header"></slot>
</tr>
</thead>
<tbody>
<!-- Body slot -->
<slot name="body"></slot>
</tbody>
</table>
</div>
</div>

<style>
/* Ensure the table is responsive */
.table-wrapper {
overflow-x: auto;
position: relative;
-webkit-overflow-scrolling: touch;
width: 100%;
}

/* Add smooth transitions for solved button */
:global(.solved-button) {
transition: all 0.3s ease;
}

/* Add hover effects */
:global(tr:hover .solved-button:not(:hover)) {
transform: scale(1.05);
}

/* Ensure feedback buttons don't get cut off */
:global(td:last-child) {
min-width: 140px;
}

/* Ensure table fits within container */
.w-full {
max-width: 100%;
}

/* Add subtle animation for solved problems */
:global(tr) {
overflow: hidden;
}

/* Checkmark animation */
:global(.solved-button) {
animation: pulse 1.5s infinite alternate;
}

:global(.checkmark-icon) {
transition: transform 0.3s ease;
}

:global(.solved-button .checkmark-icon) {
transform: scale(1.1);
}

/* Sortable header styles */
:global(th) {
transition: background-color 0.2s ease;
}

@keyframes pulse {
0% {
box-shadow: 0 0 5px rgba(34, 197, 94, 0.4);
}
100% {
box-shadow: 0 0 10px rgba(34, 197, 94, 0.7);
}
}

/* Ensure username is always purple */
:global(a[href*='github.com']) {
color: var(--color-username) !important;
}

:global(a[href*='github.com']:hover) {
color: color-mix(in oklab, var(--color-username) 80%, white) !important;
}
</style>
83 changes: 83 additions & 0 deletions src/lib/components/tables/FeedbackButtons.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';

// Event dispatcher
const dispatch = createEventDispatcher();

// Props
export let itemId: string;
export let likes: number = 0;
export let dislikes: number = 0;
export let userFeedback: 'like' | 'dislike' | null = null;
export let isAuthenticated: boolean = false;

// Function to handle like/dislike
function handleFeedback(isLike: boolean) {
if (!isAuthenticated) return;

// If already liked/disliked, toggle off
const isUndo = (isLike && userFeedback === 'like') || (!isLike && userFeedback === 'dislike');

// Dispatch event to parent component
dispatch('feedback', {
itemId,
isLike,
isUndo,
previousFeedback: userFeedback
});
}
</script>

<div class="flex justify-end gap-2">
<button
class={`flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors duration-200 ${
userFeedback === 'like'
? 'bg-[var(--color-accent)] text-white'
: 'bg-[var(--color-tertiary)] text-[var(--color-text)] hover:bg-[var(--color-accent-muted)]'
}`}
on:click={() => handleFeedback(true)}
disabled={!isAuthenticated}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-2"
>
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path>
</svg>
<span>{likes}</span>
</button>

<button
class={`flex items-center gap-1 rounded-md px-2 py-1 text-sm transition-colors duration-200 ${
userFeedback === 'dislike'
? 'bg-[var(--color-accent)] text-white'
: 'bg-[var(--color-tertiary)] text-[var(--color-text)] hover:bg-[var(--color-accent-muted)]'
}`}
on:click={() => handleFeedback(false)}
disabled={!isAuthenticated}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="stroke-2"
>
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"></path>
</svg>
<span>{dislikes}</span>
</button>
</div>
55 changes: 55 additions & 0 deletions src/lib/components/tables/SortableHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';

// Event dispatcher
const dispatch = createEventDispatcher();

// Props
export let direction: 'asc' | 'desc' | null = null;
export let label: string;
export let width: string = 'auto';

// Function to handle click
function handleInteraction() {
// Toggle direction: null -> asc -> desc -> null
if (direction === null) {
direction = 'asc';
} else if (direction === 'asc') {
direction = 'desc';
} else {
direction = null;
}

// Dispatch event to parent component
dispatch('sort', { direction });
}

// Handle keyboard events
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleInteraction();
}
}
</script>

<button
class="flex w-full cursor-pointer items-center justify-center gap-2 border-none bg-transparent p-0 text-inherit"
on:click={handleInteraction}
on:keydown={handleKeydown}
style="width: {width}"
type="button"
aria-label="Sort by {label}"
>
{#if direction === 'asc'}
<span class="text-sm font-bold text-[var(--color-accent)]">▲</span>
{:else if direction === 'desc'}
<span class="text-sm font-bold text-[var(--color-accent)]">▼</span>
{:else}
<span class="flex flex-col text-sm leading-[1] font-bold text-[var(--color-text-muted)]">
<span>▲</span>
<span>▼</span>
</span>
{/if}
<span class="font-bold">{label}</span>
</button>
82 changes: 82 additions & 0 deletions src/lib/components/tables/SourceFilter.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';

// Event dispatcher
const dispatch = createEventDispatcher();

// Props
export let state: 'all' | string = 'all';
export let options: { value: string; icon: string; color: string }[] = [];
export let title: string = 'Filter by source';

// Function to handle filter interaction
function handleInteraction() {
// Find current index
const currentIndex = options.findIndex((opt) => opt.value === state) + 1;

// If we're at the end or not found, go back to 'all'
if (currentIndex >= options.length || currentIndex === 0) {
state = 'all';
} else {
// Otherwise, go to the next option
state = options[currentIndex].value;
}

// Dispatch event to parent component
dispatch('change', { source: state === 'all' ? null : state });
}

// Handle keyboard events
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleInteraction();
}
}

// Get current option
$: currentOption =
state === 'all'
? { value: 'all', icon: '', color: 'var(--color-text-muted)' }
: options.find((opt) => opt.value === state) || {
value: 'all',
icon: '',
color: 'var(--color-text-muted)'
};

// Get current state label for aria-label
$: stateLabel = state === 'all' ? 'Show all sources' : `Show ${state} only`;
</script>

<button
class="flex cursor-pointer items-center justify-center gap-1 border-none bg-transparent p-0"
on:click={handleInteraction}
on:keydown={handleKeydown}
title={title}
type="button"
aria-label={`${title}: ${stateLabel}`}
>
{#if state === 'all'}
<span class="text-sm font-bold text-[var(--color-text-muted)]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</span>
{:else}
<span class="text-sm font-bold" style="color: {currentOption.color}">
{@html currentOption.icon}
</span>
{/if}
</button>
48 changes: 48 additions & 0 deletions src/lib/components/tables/StatusButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';

// Event dispatcher
const dispatch = createEventDispatcher();

// Props
export let itemId: string;
export let isActive: boolean = false;
export let isAuthenticated: boolean = false;

// Function to handle status toggle
function handleToggle() {
if (!isAuthenticated) return;

// Dispatch event to parent component
dispatch('toggle', {
itemId,
isActive: !isActive
});
}
</script>

<button
class={`flex h-8 w-8 cursor-pointer items-center justify-center rounded-full shadow-sm transition-all duration-300
${isActive
? 'solved-button bg-[rgb(34_197_94)] text-white shadow-[0_0_8px_rgba(34,197,94,0.4)]'
: 'bg-[var(--color-tertiary)] text-[var(--color-text-muted)] hover:bg-[var(--color-accent-muted)] hover:text-[var(--color-accent)]'
}`}
on:click={handleToggle}
disabled={!isAuthenticated}
aria-label={isActive ? "Mark as inactive" : "Mark as active"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="checkmark-icon"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</button>
Loading