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
5 changes: 4 additions & 1 deletion frontend/src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAnaglyph } from '@/composables/useAnaglyph';
import { useAssessmentDetailStore } from '@/stores/assessmentDetail';
import { useAuthStore } from '@/stores/auth';
import { usePreferencesStore } from '@/stores/preferences';
Expand All @@ -31,6 +32,7 @@ const route = useRoute();
const isDark = useDark();
const toggleDark = useToggle(isDark);
const showAboutModal = ref(false);
const { registerClick: onLogoClick } = useAnaglyph();

// Breadcrumb: show assessment name when on assessment or activity routes
const assessmentId = computed(() => route.params.id as string | undefined);
Expand Down Expand Up @@ -71,7 +73,7 @@ const handleLogout = async () => {
<!-- Logo + Breadcrumb -->
<div class="flex items-center gap-1 min-w-0">
<h1 class="text-2xl font-bold shrink-0">
<RouterLink to="/" class="hover:opacity-80 transition-opacity">RAPTR</RouterLink>
<RouterLink to="/" class="hover:opacity-80 transition-opacity" @click="onLogoClick">RAPTR</RouterLink>
</h1>
<template v-if="isAssessmentRoute && assessmentName">
<ChevronRight class="h-4 w-4 text-muted-foreground shrink-0" />
Expand Down Expand Up @@ -155,6 +157,7 @@ const handleLogout = async () => {
<!-- About Modal -->
<AboutModal v-model:open="showAboutModal" />


<!-- Logout -->
<Button @click="handleLogout" variant="destructive" size="sm" class="ml-2">
<LogOut class="mr-2 h-4 w-4" />
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/composables/useAnaglyph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ref, watch } from 'vue';

const REQUIRED_CLICKS = 5;
const TIME_WINDOW_MS = 2000;
const HTML_CLASS = 'anaglyph-active';

/**
* Anaglyph composable — tracks rapid clicks and toggles a whole-page
* anaglyph effect by adding/removing a CSS class on <html>.
*
* Call `registerClick()` on each click. If 5 clicks land within 2 seconds,
* the effect toggles on/off.
*/
export function useAnaglyph() {
const activated = ref(false);
const clickTimestamps: number[] = [];

function registerClick() {
const now = Date.now();
clickTimestamps.push(now);

// Keep only clicks within the time window
while (
clickTimestamps.length > 0 &&
now - clickTimestamps[0] > TIME_WINDOW_MS
) {
clickTimestamps.shift();
}

if (clickTimestamps.length >= REQUIRED_CLICKS) {
activated.value = !activated.value;
clickTimestamps.length = 0;
}
}

// Sync the CSS class on <html>
watch(activated, (active) => {
if (active) {
document.documentElement.classList.add(HTML_CLASS);
} else {
document.documentElement.classList.remove(HTML_CLASS);
}
});

return { activated, registerClick };
}
218 changes: 183 additions & 35 deletions frontend/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,41 @@
@custom-variant dark (&:is(.dark *));

@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

:root {
Expand Down Expand Up @@ -131,3 +131,151 @@
html {
font-size: 14px;
}

/* ══════════════════════════════════════════════════════════
Whole-page anaglyph 3D effect
Activated by toggling .anaglyph-active on <html>
══════════════════════════════════════════════════════════ */

/* Smooth transition for entering / leaving the effect */
html.anaglyph-active * {
transition: text-shadow 0.4s ease, box-shadow 0.4s ease, filter 0.4s ease;
}

/* ── Text: red/cyan text-shadow on everything ────────────── */
html.anaglyph-active * {
text-shadow:
-3px 0 rgba(255, 0, 0, 0.7),
3px 0 rgba(0, 255, 255, 0.7) !important;
animation: anaglyph-text-wobble 3s ease-in-out infinite;
}

/* ── SVG icons (Lucide etc.) ─────────────────────────────── */
html.anaglyph-active svg {
filter:
drop-shadow(-2px 0 rgba(255, 0, 0, 0.65)) drop-shadow(2px 0 rgba(0, 255, 255, 0.65));
animation: anaglyph-icon-wobble 3s ease-in-out infinite;
}

/* ── Images ──────────────────────────────────────────────── */
html.anaglyph-active img {
filter:
drop-shadow(-3px 0 rgba(255, 0, 0, 0.5)) drop-shadow(3px 0 rgba(0, 255, 255, 0.5));
}

/* ── Interactive elements: buttons, inputs ───────────────── */
html.anaglyph-active button,
html.anaglyph-active [role="button"],
html.anaglyph-active input,
html.anaglyph-active select,
html.anaglyph-active textarea {
box-shadow:
-3px 0 0 rgba(255, 0, 0, 0.25),
3px 0 0 rgba(0, 255, 255, 0.25) !important;
}

/* ── Cards, tables, bordered containers ──────────────────── */
html.anaglyph-active [data-slot="card"],
html.anaglyph-active table,
html.anaglyph-active .border,
html.anaglyph-active [class*="rounded-lg"][class*="border"] {
box-shadow:
-3px 0 0 rgba(255, 0, 0, 0.18),
3px 0 0 rgba(0, 255, 255, 0.18);
}

/* ── Top nav bar ─────────────────────────────────────────── */
html.anaglyph-active nav.border-b {
box-shadow:
0 2px 0 rgba(255, 0, 0, 0.2),
0 -2px 0 rgba(0, 255, 255, 0.2);
}

/* ── CRT scanline overlay ────────────────────────────────── */
html.anaglyph-active body::after {
content: '';
position: fixed;
inset: 0;
z-index: 99999;
pointer-events: none;
background: repeating-linear-gradient(0deg,
rgba(255, 255, 255, 0.025) 0px,
rgba(255, 255, 255, 0.025) 1px,
transparent 1px,
transparent 3px);
opacity: 0;
animation: anaglyph-fade-in 0.5s ease forwards, anaglyph-scanline-scroll 8s linear infinite;
}

/* ── Wobble animations ───────────────────────────────────── */
@keyframes anaglyph-text-wobble {

0%,
100% {
text-shadow:
-3px 0 rgba(255, 0, 0, 0.7),
3px 0 rgba(0, 255, 255, 0.7);
}

25% {
text-shadow:
-4px 1px rgba(255, 0, 0, 0.75),
4px -1px rgba(0, 255, 255, 0.75);
}

50% {
text-shadow:
-2px -0.5px rgba(255, 0, 0, 0.65),
2px 0.5px rgba(0, 255, 255, 0.65);
}

75% {
text-shadow:
-4px 0.5px rgba(255, 0, 0, 0.7),
4px -0.5px rgba(0, 255, 255, 0.7);
}
}

@keyframes anaglyph-icon-wobble {

0%,
100% {
filter:
drop-shadow(-2px 0 rgba(255, 0, 0, 0.65)) drop-shadow(2px 0 rgba(0, 255, 255, 0.65));
}

25% {
filter:
drop-shadow(-3px 0.5px rgba(255, 0, 0, 0.7)) drop-shadow(3px -0.5px rgba(0, 255, 255, 0.7));
}

50% {
filter:
drop-shadow(-1.5px -0.5px rgba(255, 0, 0, 0.6)) drop-shadow(1.5px 0.5px rgba(0, 255, 255, 0.6));
}

75% {
filter:
drop-shadow(-3px 0.3px rgba(255, 0, 0, 0.65)) drop-shadow(3px -0.3px rgba(0, 255, 255, 0.65));
}
}

@keyframes anaglyph-scanline-scroll {
0% {
background-position: 0 0;
}

100% {
background-position: 0 100px;
}
}

@keyframes anaglyph-fade-in {
from {
opacity: 0;
}

to {
opacity: 1;
}
}
Loading