Skip to content

Commit 02c6d7b

Browse files
authored
Added seasonal modifications to the streambert icon in-app (christmas, halloween, pride) (#141)
* added Seasonal events - there is now an endpoint to have season specific modifications of the streambert icon in the sidebar - right now there are pride, halloween and christmas * improved halloween icon - halloween icon is now a carved pumpkin - tested all seasonal designs properly
1 parent 4db8738 commit 02c6d7b

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

src/components/Sidebar.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useRef, useEffect } from "react";
22
import { imgUrl } from "../utils/api";
3+
import { useSeasonalEvent } from "../components/seasonalEvents.jsx";
34
import {
45
StreambertLogo,
56
HomeIcon,
@@ -28,6 +29,7 @@ export default function Sidebar({
2829
const [dragOver, setDragOver] = useState(null);
2930
const dragItem = useRef(null);
3031
const dragNode = useRef(null);
32+
const seasonalEvent = useSeasonalEvent();
3133

3234
const [tooltip, setTooltip] = useState(null); // { title, y }
3335
const [contextMenu, setContextMenu] = useState(null); // { item, x, y }
@@ -104,8 +106,10 @@ export default function Sidebar({
104106
className="sidebar-logo"
105107
onClick={() => onNavigate("home")}
106108
title="Streambert"
109+
style={{ position: "relative" }}
107110
>
108111
<StreambertLogo />
112+
{seasonalEvent && seasonalEvent.render()}
109113
</div>
110114

111115
{canGoBack && (

src/components/seasonalEvents.jsx

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/**
2+
* Seasonal / event-based logo overlays for the Streambert sidebar.
3+
*
4+
* To add a new event:
5+
* 1. Add an entry to EVENTS below.
6+
* 2. `check(now)` receives a Date (returns true when date is active)
7+
* 3. `render()` returns a React element that overlays the logo.
8+
* Keep it contained within a 40×40 px area (the logo wrapper size).
9+
*
10+
* Events are evaluated top-to-bottom; the first match wins.
11+
*/
12+
13+
import { useEffect, useState } from "react";
14+
15+
// ── Event definitions ────────────────────────────────────────────────────────
16+
17+
const EVENTS = [
18+
// ── Pride Month (June) ───────────────────────────────────────────────────
19+
{
20+
id: "pride",
21+
check: (d) => d.getMonth() === 5, // June = 5
22+
render: () => <PrideOverlay />,
23+
},
24+
25+
// ── Christmas (Dec 20 – Dec 31) ──────────────────────────────────────────
26+
{
27+
id: "christmas",
28+
check: (d) => d.getMonth() === 11 && d.getDate() >= 20,
29+
render: () => <SnowOverlay />,
30+
},
31+
32+
// ── New Year (Jan 1 – Jan 5) ─────────────────────────────────────────────
33+
{
34+
id: "newyear",
35+
check: (d) => d.getMonth() === 0 && d.getDate() <= 5,
36+
render: () => <SnowOverlay />,
37+
},
38+
39+
// ── Halloween (Oct 24 – Oct 31) ──────────────────────────────────────────
40+
{
41+
id: "halloween",
42+
check: (d) => d.getMonth() === 9 && d.getDate() >= 24,
43+
render: () => <HalloweenOverlay />,
44+
},
45+
];
46+
47+
// ── Hook ─────────────────────────────────────────────────────────────────────
48+
49+
export function useSeasonalEvent() {
50+
const [event, setEvent] = useState(() => getActiveEvent());
51+
52+
// Re-check at midnight so the overlay appears/disappears without a reload
53+
useEffect(() => {
54+
const msUntilMidnight = () => {
55+
const now = new Date();
56+
const midnight = new Date(now);
57+
midnight.setHours(24, 0, 0, 0);
58+
return midnight - now;
59+
};
60+
61+
let timeout;
62+
const schedule = () => {
63+
timeout = setTimeout(() => {
64+
setEvent(getActiveEvent());
65+
schedule();
66+
}, msUntilMidnight());
67+
};
68+
schedule();
69+
return () => clearTimeout(timeout);
70+
}, []);
71+
72+
return event;
73+
}
74+
75+
/**
76+
* To test an event temporarily, change getActiveEvent() to:
77+
* const now = new Date("2024-10-28"); // Halloween
78+
* const now = new Date("2024-12-25"); // Christmas
79+
* const now = new Date("2024-06-15"); // Pride
80+
*
81+
* To make it normal again:
82+
* const now = new Date();
83+
*/
84+
function getActiveEvent() {
85+
const now = new Date();
86+
return EVENTS.find((e) => e.check(now)) ?? null;
87+
}
88+
89+
// ── Pride overlay ─────────────────────────────────────────────────────────────
90+
// Small pride flag in the bottom-right corner of the logo.
91+
92+
const PRIDE_STRIPES = [
93+
"#FF0018",
94+
"#FFA52C",
95+
"#FFFF41",
96+
"#008018",
97+
"#0000F9",
98+
"#86007D",
99+
];
100+
101+
function PrideOverlay() {
102+
return (
103+
<div style={overlayBase}>
104+
{/* tiny flag in bottom-right corner */}
105+
<div
106+
style={{
107+
position: "absolute",
108+
bottom: 3,
109+
right: 2,
110+
width: 14,
111+
height: 10,
112+
borderRadius: 1,
113+
overflow: "hidden",
114+
display: "flex",
115+
flexDirection: "column",
116+
boxShadow: "0 0 0 1px rgba(0,0,0,0.25)",
117+
}}
118+
>
119+
{PRIDE_STRIPES.map((color, i) => (
120+
<div key={i} style={{ flex: 1, background: color }} />
121+
))}
122+
</div>
123+
</div>
124+
);
125+
}
126+
127+
// ── Snow overlay ─────────────────────────────────────────────────────────────
128+
129+
const SNOWFLAKES = Array.from({ length: 7 }, (_, i) => ({
130+
left: `${10 + i * 13}%`,
131+
delay: `${i * 0.4}s`,
132+
duration: `${1.6 + (i % 3) * 0.5}s`,
133+
size: i % 2 === 0 ? 3 : 2,
134+
}));
135+
136+
function SnowOverlay() {
137+
return (
138+
<div style={{ ...overlayBase, overflow: "hidden" }}>
139+
<style>{`
140+
@keyframes sb-snow-fall {
141+
0% { transform: translateY(-6px) rotate(0deg); opacity: 0; }
142+
15% { opacity: 1; }
143+
85% { opacity: 1; }
144+
100% { transform: translateY(44px) rotate(180deg); opacity: 0; }
145+
}
146+
`}</style>
147+
{SNOWFLAKES.map((s, i) => (
148+
<div
149+
key={i}
150+
style={{
151+
position: "absolute",
152+
left: s.left,
153+
top: 0,
154+
width: s.size,
155+
height: s.size,
156+
borderRadius: "50%",
157+
background: "#fff",
158+
opacity: 0,
159+
animation: `sb-snow-fall ${s.duration} ${s.delay} infinite linear`,
160+
}}
161+
/>
162+
))}
163+
</div>
164+
);
165+
}
166+
167+
// ── Halloween overlay ─────────────────────────────────────────────────────────
168+
169+
function HalloweenOverlay() {
170+
return (
171+
<div style={overlayBase}>
172+
{/* tiny pumpkin in bottom-right corner */}
173+
<div
174+
style={{
175+
position: "absolute",
176+
bottom: 1,
177+
right: 1,
178+
width: 14,
179+
height: 14,
180+
display: "flex",
181+
alignItems: "center",
182+
justifyContent: "center",
183+
fontSize: 12,
184+
lineHeight: 1,
185+
}}
186+
>
187+
🎃
188+
</div>
189+
</div>
190+
);
191+
}
192+
193+
// ── Shared styles ─────────────────────────────────────────────────────────────
194+
195+
const overlayBase = {
196+
position: "absolute",
197+
inset: 0,
198+
borderRadius: "inherit",
199+
display: "flex",
200+
flexDirection: "column",
201+
pointerEvents: "none",
202+
overflow: "hidden",
203+
};

0 commit comments

Comments
 (0)