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
132 changes: 130 additions & 2 deletions database_schema_definition.sql
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ AFTER
INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- Create problems table if it doesn't exist
CREATE TABLE IF NOT EXISTS problems (
id TEXT PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
tags TEXT [] NOT NULL DEFAULT '{}',
difficulty INTEGER,
Expand Down Expand Up @@ -86,4 +86,132 @@ UPDATE USING (
)
);
-- Disable deletion of problems entirely
-- We don't want to allow deletion of problems at all
-- We don't want to allow deletion of problems at all
-- Create user_problem_feedback table to store user likes/dislikes
CREATE TABLE IF NOT EXISTS user_problem_feedback (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
problem_id UUID NOT NULL REFERENCES problems(id) ON DELETE CASCADE,
feedback_type TEXT NOT NULL CHECK (feedback_type IN ('like', 'dislike')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, problem_id)
);
-- Create RLS policies for user_problem_feedback table
ALTER TABLE user_problem_feedback ENABLE ROW LEVEL SECURITY;
-- Users can read all feedback (needed for displaying aggregated likes/dislikes)
CREATE POLICY "Anyone can read feedback" ON user_problem_feedback FOR
SELECT USING (true);
-- Users can only insert/update their own feedback
CREATE POLICY "Users can insert their own feedback" ON user_problem_feedback FOR
INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update their own feedback" ON user_problem_feedback FOR
UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete their own feedback" ON user_problem_feedback FOR DELETE USING (auth.uid() = user_id);
-- Create trigger function to update the updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger for user_problem_feedback table
CREATE TRIGGER update_user_problem_feedback_updated_at BEFORE
UPDATE ON user_problem_feedback FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Create stored procedure for updating problem feedback in a transaction
CREATE OR REPLACE FUNCTION update_problem_feedback(
p_problem_id UUID,
p_user_id UUID,
p_is_like BOOLEAN,
p_is_undo BOOLEAN DEFAULT FALSE,
p_previous_feedback TEXT DEFAULT NULL
) RETURNS SETOF problems AS $$
DECLARE v_problem problems %ROWTYPE;
v_feedback_exists BOOLEAN;
v_feedback_type TEXT;
BEGIN -- Lock the problem record for update to prevent race conditions
SELECT * INTO v_problem
FROM problems
WHERE id = p_problem_id FOR
UPDATE;
IF NOT FOUND THEN RAISE EXCEPTION 'Problem with ID % not found',
p_problem_id;
END IF;
-- Check if user already has feedback for this problem
SELECT EXISTS(
SELECT 1
FROM user_problem_feedback
WHERE user_id = p_user_id
AND problem_id = p_problem_id
) INTO v_feedback_exists;
IF v_feedback_exists THEN
SELECT feedback_type INTO v_feedback_type
FROM user_problem_feedback
WHERE user_id = p_user_id
AND problem_id = p_problem_id;
END IF;
-- Handle different feedback scenarios
IF p_is_undo THEN -- Undoing previous action
IF p_is_like THEN -- Undo a like
UPDATE problems
SET likes = GREATEST(0, likes - 1)
WHERE id = p_problem_id;
ELSE -- Undo a dislike
UPDATE problems
SET dislikes = GREATEST(0, dislikes - 1)
WHERE id = p_problem_id;
END IF;
-- Remove the feedback record
DELETE FROM user_problem_feedback
WHERE user_id = p_user_id
AND problem_id = p_problem_id;
ELSIF p_previous_feedback = 'like'
AND NOT p_is_like THEN -- Switching from like to dislike
UPDATE problems
SET likes = GREATEST(0, likes - 1),
dislikes = dislikes + 1
WHERE id = p_problem_id;
-- Update the feedback record
UPDATE user_problem_feedback
SET feedback_type = 'dislike'
WHERE user_id = p_user_id
AND problem_id = p_problem_id;
ELSIF p_previous_feedback = 'dislike'
AND p_is_like THEN -- Switching from dislike to like
UPDATE problems
SET likes = likes + 1,
dislikes = GREATEST(0, dislikes - 1)
WHERE id = p_problem_id;
-- Update the feedback record
UPDATE user_problem_feedback
SET feedback_type = 'like'
WHERE user_id = p_user_id
AND problem_id = p_problem_id;
ELSE -- New feedback
IF p_is_like THEN -- New like
UPDATE problems
SET likes = likes + 1
WHERE id = p_problem_id;
ELSE -- New dislike
UPDATE problems
SET dislikes = dislikes + 1
WHERE id = p_problem_id;
END IF;
-- Insert new feedback record
INSERT INTO user_problem_feedback (user_id, problem_id, feedback_type)
VALUES (
p_user_id,
p_problem_id,
CASE
WHEN p_is_like THEN 'like'
ELSE 'dislike'
END
);
END IF;
-- Return the updated problem
RETURN QUERY
SELECT *
FROM problems
WHERE id = p_problem_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Ensure the function is accessible to authenticated users
GRANT EXECUTE ON FUNCTION update_problem_feedback TO authenticated;
37 changes: 25 additions & 12 deletions src/lib/components/ProblemTable.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import type { Problem } from '$lib/services/problem';
import { user } from '$lib/services/auth';
// Use static image paths instead of imports
const codeforcesLogo = '/images/codeforces.png';
const kattisLogo = '/images/kattis.png';
Expand All @@ -9,6 +10,14 @@ export let problems: Problem[] = [];
export let userFeedback: Record<string, 'like' | 'dislike' | null> = {};
export let onLike: (problemId: string, isLike: boolean) => Promise<void>;

// State
let isAuthenticated = false;

// Subscribe to auth state
user.subscribe((value) => {
isAuthenticated = !!value;
});

// Define common tiers
const TIERS = [
[3000, 'Legendary Grandmaster'],
Expand Down Expand Up @@ -144,9 +153,14 @@ function getDifficultyTooltip(problem: Problem): string {
${hasLiked
? 'border-[color-mix(in_oklab,rgb(34_197_94)_50%,transparent)] bg-[color-mix(in_oklab,rgb(34_197_94)_10%,transparent)] text-[rgb(34_197_94)]'
: 'border-[var(--color-border)] bg-transparent text-[var(--color-text)] hover:border-[color-mix(in_oklab,rgb(34_197_94)_50%,transparent)] hover:bg-[color-mix(in_oklab,rgb(34_197_94)_10%,transparent)] hover:text-[rgb(34_197_94)]'
}`}
on:click={() => onLike(problem.id!, true)}
title={hasLiked ? 'Undo like' : 'Like this problem'}
} ${!isAuthenticated ? 'cursor-not-allowed opacity-50' : ''}`}
on:click={() => isAuthenticated && onLike(problem.id!, true)}
title={!isAuthenticated
? 'Sign in to like problems'
: hasLiked
? 'Undo like'
: 'Like this problem'}
disabled={!isAuthenticated}
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -172,9 +186,14 @@ function getDifficultyTooltip(problem: Problem): string {
${hasDisliked
? 'border-[color-mix(in_oklab,rgb(239_68_68)_50%,transparent)] bg-[color-mix(in_oklab,rgb(239_68_68)_10%,transparent)] text-[rgb(239_68_68)]'
: 'border-[var(--color-border)] bg-transparent text-[var(--color-text)] hover:border-[color-mix(in_oklab,rgb(239_68_68)_50%,transparent)] hover:bg-[color-mix(in_oklab,rgb(239_68_68)_10%,transparent)] hover:text-[rgb(239_68_68)]'
}`}
on:click={() => onLike(problem.id!, false)}
title={hasDisliked ? 'Undo dislike' : 'Dislike this problem'}
} ${!isAuthenticated ? 'cursor-not-allowed opacity-50' : ''}`}
on:click={() => isAuthenticated && onLike(problem.id!, false)}
title={!isAuthenticated
? 'Sign in to dislike problems'
: hasDisliked
? 'Undo dislike'
: 'Dislike this problem'}
disabled={!isAuthenticated}
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -198,12 +217,6 @@ function getDifficultyTooltip(problem: Problem): string {
</div>
</td>
</tr>
{:else}
<tr>
<td colspan="6" class="p-6 text-center text-[var(--color-text-muted)]">
No problems found with the selected filter.
</td>
</tr>
{/each}
</tbody>
</table>
Expand Down
35 changes: 21 additions & 14 deletions src/lib/components/TopicSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,15 @@ onMount(() => {

<!-- Desktop sidebar (always visible) -->
<div
class="absolute top-[60px] bottom-auto left-0 hidden w-[13rem] border-r border-[var(--color-border)] bg-[var(--color-primary)] md:block"
class="absolute top-[60px] bottom-auto left-0 hidden w-[13rem] bg-[var(--color-primary)] md:block"
>
<div class="h-[calc(100vh-140px)] overflow-y-auto">
<div class="w-full space-y-1 p-3">
<button
class={`w-full rounded-md px-3 py-2 text-left transition-colors ${
class={`w-full rounded-md px-3 py-2 text-left transition-all duration-200 ${
selectedTopic === null
? 'bg-[var(--color-secondary)] text-white'
: 'text-[var(--color-text)] hover:bg-[var(--color-tertiary)]'
? 'scale-102 bg-[var(--color-secondary)] text-white shadow-sm'
: 'text-[var(--color-text)] hover:scale-102 hover:bg-[var(--color-tertiary)] hover:shadow-sm'
}`}
on:click={() => onSelectTopic(null)}
>
Expand All @@ -116,10 +116,10 @@ onMount(() => {

{#each topics as topic}
<button
class={`w-full rounded-md px-3 py-2 text-left transition-colors ${
class={`w-full rounded-md px-3 py-2 text-left transition-all duration-200 ${
selectedTopic === topic
? 'bg-[var(--color-secondary)] text-white'
: 'text-[var(--color-text)] hover:bg-[var(--color-tertiary)]'
? 'scale-102 bg-[var(--color-secondary)] text-white shadow-sm'
: 'text-[var(--color-text)] hover:scale-102 hover:bg-[var(--color-tertiary)] hover:shadow-sm'
}`}
on:click={() => onSelectTopic(topic)}
>
Expand All @@ -138,10 +138,10 @@ onMount(() => {
<div class="h-full overflow-y-auto p-4 pt-16">
<div class="space-y-1">
<button
class={`w-full rounded-md px-3 py-2 text-left transition-colors ${
class={`w-full rounded-md px-3 py-2 text-left transition-all duration-200 ${
selectedTopic === null
? 'bg-[var(--color-primary)] text-white'
: 'text-[var(--color-text)] hover:bg-[var(--color-tertiary)]'
? 'scale-102 bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--color-text)] hover:scale-102 hover:bg-[var(--color-tertiary)] hover:shadow-sm'
}`}
on:click={() => onSelectTopic(null)}
>
Expand All @@ -150,10 +150,10 @@ onMount(() => {

{#each topics as topic}
<button
class={`w-full rounded-md px-3 py-2 text-left transition-colors ${
class={`w-full rounded-md px-3 py-2 text-left transition-all duration-200 ${
selectedTopic === topic
? 'bg-[var(--color-primary)] text-white'
: 'text-[var(--color-text)] hover:bg-[var(--color-tertiary)]'
? 'scale-102 bg-[var(--color-primary)] text-white shadow-sm'
: 'text-[var(--color-text)] hover:scale-102 hover:bg-[var(--color-tertiary)] hover:shadow-sm'
}`}
on:click={() => onSelectTopic(topic)}
>
Expand All @@ -178,7 +178,14 @@ onMount(() => {
z-index: 40;
}

/* Remove unused styles */
/* Add hover animation scale */
:global(.scale-102) {
transform: scale(1.02);
}

:global(.hover\:scale-102:hover) {
transform: scale(1.02);
}

/* Ensure sidebar doesn't overlap footer */
@media (min-width: 768px) {
Expand Down
Loading