Holiday Frogs ❤️🍀🎃🦃🎅 (#1766)

* Holiday Frogs ❤️🍀🎃🦃🎅

* Rework to make it easier to inject a different Frog
This commit is contained in:
Mark Street
2025-12-10 09:03:35 +00:00
committed by GitHub
parent c8da8c6762
commit ac06780e6d
15 changed files with 175 additions and 115 deletions

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import Link from "next/link";
import Frog from "@/components/Nav/frog.svg";
import Frog from "@/components/Frog/Frog";
const subtitle = "mt-8 text-xl font-semibold tracking-tight text-gray-11";

View File

@@ -1,7 +1,7 @@
import { ChevronRightIcon } from "@primer/octicons-react";
import GhostButton from "@/components/GhostButton";
import Frog from "@/components/Nav/frog.svg";
import Frog from "@/components/Frog/Frog";
export default function NotFound() {
return (

View File

@@ -4,7 +4,7 @@ import { MarkGithubIcon } from "@primer/octicons-react";
import DiscordIcon from "./discord.svg";
import GhostButton from "./GhostButton";
import Logotype from "./Logotype";
import SiteLogo from "./Nav/SiteLogo";
function Separator() {
return <div className="hidden h-4 w-px bg-gray-6 sm:inline-block" />;
@@ -20,7 +20,7 @@ export default function Footer() {
<div className="border-gray-6 border-t py-10">
<div className="flex items-center justify-center">
<Link href="/">
<Logotype />
<SiteLogo />
</Link>
</div>
<div className="mt-4 flex flex-col items-center justify-center gap-1 sm:flex-row sm:gap-2">

View File

@@ -1,12 +1,8 @@
import type { SVGProps } from "react";
import clsx from "clsx";
import type * as api from "@/lib/api";
import Frog from "../Nav/frog.svg";
import styles from "./AnonymousFrog.module.scss";
import Frog from "./Frog";
export type Props = SVGProps<SVGElement> & {
user: api.AnonymousUser;
@@ -18,11 +14,11 @@ export default function AnonymousFrogAvatar({
className,
...props
}: Props) {
const accentStyle = {
"--accent-hue": user.frog_color[0],
"--accent-saturation": user.frog_color[1],
"--accent-lightness": user.frog_color[2],
};
const [hue, saturation, lightness] = user.frog_color;
const primary = `hsl(${hue}, ${saturation * 100}%, ${lightness * 100}%)`;
const secondary = `hsl(${hue}, ${(0.3 * (1 - saturation) + saturation) * 100}%, ${(0.5 * (1 - lightness) + lightness) * 100}%)`;
const nose = `hsl(${hue}, ${(saturation - 0.2 * saturation) * 100}%, ${(lightness - 0.4 * lightness) * 100}%)`;
return (
<div
@@ -32,8 +28,10 @@ export default function AnonymousFrogAvatar({
)}
>
<Frog
style={accentStyle}
className={clsx(styles.anonymousFrog, "h-4/6 w-4/6")}
primary={primary}
secondary={secondary}
nose={nose}
className="h-4/6 w-4/6"
{...props}
/>
</div>

View File

@@ -0,0 +1,31 @@
import React from "react";
const Frog = ({
primary = "#951fd9",
secondary = "#cc87f4",
pupil = "#000",
eye = "#FFF",
nose = "#505050",
title = "",
...props
}) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" {...props}>
<title>{title}</title>
<path
fill={secondary}
d="M36 22c0 7.456-8.059 12-18 12S0 29.456 0 22 8.059 7 18 7s18 7.544 18 15z"
/>
<path
fill={primary}
d="M31.755 12.676C33.123 11.576 34 9.891 34 8c0-3.313-2.687-6-6-6-2.861 0-5.25 2.004-5.851 4.685-1.288-.483-2.683-.758-4.149-.758-1.465 0-2.861.275-4.149.758C13.25 4.004 10.861 2 8 2 4.687 2 2 4.687 2 8c0 1.891.877 3.576 2.245 4.676C1.6 15.356 0 18.685 0 22c0 7.456 8.059 1 18 1s18 6.456 18-1c0-3.315-1.6-6.644-4.245-9.324z"
/>
<circle fill={eye} cx="7.5" cy="7.5" r="3.5" className="eyeL" />
<circle fill={pupil} cx="7.5" cy="7.5" r="1.5" className="pupilL" />
<circle fill={eye} cx="28.5" cy="7.5" r="3.5" className="eyeR" />
<circle fill={pupil} cx="28.5" cy="7.5" r="1.5" className="pupilR" />
<circle fill={nose} cx="14" cy="20" r="1" className="noseL" />
<circle fill={nose} cx="22" cy="20" r="1" className="noseR" />
</svg>
);
export default Frog;

View File

@@ -0,0 +1,66 @@
import React from "react";
import Frog from "./Frog";
interface HolidayFrogProps {
today?: Date;
className?: string;
}
export default function HolidayFrog({
today = new Date(),
className = "size-7",
}: HolidayFrogProps) {
const month = today.getUTCMonth();
const date = today.getUTCDate();
// TODO: special handling for frug
// Defaults
let primary = "#951fd9";
let secondary = "#cc87f4";
let message = "Happy Decomping!";
if (month === 1 && date === 14) {
primary = "#e6396f";
secondary = "#f7a1c4";
message = "❤️ Happy Valentine's Day! ❤️";
}
if (month === 2 && date === 17) {
primary = "#2a9d34";
secondary = "#7fd28d";
message = "🍀 Happy St Patrick's Day! 🍀";
}
if (month === 9 && date === 31) {
primary = "#d96516";
secondary = "#20150aff";
message = "🎃 Happy Halloween! 🎃";
}
if (month === 10) {
// Thanksgiving (4th Thursday of November)
const firstDay = new Date(today.getFullYear(), 10, 1);
const firstThursday = 1 + ((4 - firstDay.getDay() + 7) % 7);
const fourthThursday = firstThursday + 21;
if (date === fourthThursday) {
primary = "#9e682a";
secondary = "#d2a679";
message = "🦃 Happy Thanksgiving! 🦃";
}
}
if (month === 11 && date === 25) {
primary = "#E61A1A";
secondary = "#F8BFBF";
message = "🎅 Merry Christmas! 🎅";
}
return (
<Frog
className={className}
aria-label="Purple frog"
primary={primary}
secondary={secondary}
title={message}
/>
);
}

View File

@@ -1,15 +0,0 @@
import Frog from "./Nav/frog.svg";
export default function Logotype() {
return (
<div
className="inline-flex items-center space-x-2"
aria-label="decomp.me logo"
>
<Frog className="size-7" aria-label="Purple frog" />
<span className="font-semibold text-xl leading-6 tracking-tight">
decomp.me
</span>
</div>
);
}

View File

@@ -64,47 +64,6 @@ $padding: 8px;
width: 100%;
height: 100%;
}
&:not(:hover) {
@keyframes blink {
0% {
transform: scaleY(1);
opacity: 1;
}
25% {
transform: scaleY(0.2);
opacity: 0;
}
50% {
transform: scaleY(1);
opacity: 1;
}
75% {
transform: scaleY(0.2);
opacity: 0;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
:global(.frog_svg__pupilR),
:global(.frog_svg__pupilL),
:global(.frog_svg__eyeR),
:global(.frog_svg__eyeL) {
transform-origin: 0 7px;
animation: blink 0.4s 2s ease;
@media (prefers-reduced-motion) {
animation: none;
}
}
}
}
.menu {

View File

@@ -9,7 +9,7 @@ import { ThreeBarsIcon, XIcon } from "@primer/octicons-react";
import clsx from "clsx";
import GhostButton from "../GhostButton";
import Logotype from "../Logotype";
import SiteLogo from "./SiteLogo";
import LoginState from "./LoginState";
import styles from "./Nav.module.scss";
@@ -71,7 +71,7 @@ export default function Nav({ children }: Props) {
href="/"
className="transition-colors hover:text-gray-12 active:translate-y-px"
>
<Logotype />
<SiteLogo />
</Link>
</li>
<li className={styles.headerItemLoginState}>
@@ -108,7 +108,7 @@ export default function Nav({ children }: Props) {
<ul className={styles.links}>
<li className="flex items-center justify-center">
<Link href="/">
<Logotype />
<SiteLogo />
</Link>
</li>
<li>

View File

@@ -0,0 +1,39 @@
@keyframes blink {
0% {
transform: scaleY(1);
opacity: 1;
}
25% {
transform: scaleY(0.2);
opacity: 0;
}
50% {
transform: scaleY(1);
opacity: 1;
}
75% {
transform: scaleY(0.2);
opacity: 0;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
.blinkingEyes :global(.pupilR),
.blinkingEyes :global(.pupilL),
.blinkingEyes :global(.eyeR),
.blinkingEyes :global(.eyeL) {
transform-origin: 0 7px;
animation: blink 0.4s 2s ease;
}
@media (prefers-reduced-motion) {
.blinkingEyes :global(.pupilR),
.blinkingEyes :global(.pupilL),
.blinkingEyes :global(.eyeR),
.blinkingEyes :global(.eyeL) {
animation: none;
}
}

View File

@@ -0,0 +1,20 @@
import clsx from "clsx";
import HolidayFrog from "../Frog/HolidayFrog";
import styles from "./SiteLogo.module.scss";
export default function SiteLogo() {
return (
<div
className={clsx(
"inline-flex items-center space-x-2",
styles.blinkingEyes,
)}
aria-label="decomp.me logo"
>
<HolidayFrog />
<span className="font-semibold text-xl leading-6 tracking-tight">
decomp.me
</span>
</div>
);
}

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<title>decomp.me</title>
<path fill="var(--frog-secondary)" d="M36 22c0 7.456-8.059 12-18 12S0 29.456 0 22 8.059 7 18 7s18 7.544 18 15z"/>
<path fill="var(--frog-primary)" d="M31.755 12.676C33.123 11.576 34 9.891 34 8c0-3.313-2.687-6-6-6-2.861 0-5.25 2.004-5.851 4.685-1.288-.483-2.683-.758-4.149-.758-1.465 0-2.861.275-4.149.758C13.25 4.004 10.861 2 8 2 4.687 2 2 4.687 2 8c0 1.891.877 3.576 2.245 4.676C1.6 15.356 0 18.685 0 22c0 7.456 8.059 1 18 1s18 6.456 18-1c0-3.315-1.6-6.644-4.245-9.324z"/>
<circle fill="#FFF" cx="7.5" cy="7.5" r="3.5" class="eyeL" />
<circle fill="var(--frog-pupil)" cx="7.5" cy="7.5" r="1.5" class="pupilL" />
<circle fill="#FFF" cx="28.5" cy="7.5" r="3.5" class="eyeR" />
<circle fill="var(--frog-pupil)" cx="28.5" cy="7.5" r="1.5" class="pupilR" />
<circle fill="var(--frog-nose)" cx="14" cy="20" r="1"/>
<circle fill="var(--frog-nose)" cx="22" cy="20" r="1"/>
</svg>

Before

Width:  |  Height:  |  Size: 995 B

View File

@@ -13,7 +13,7 @@ import { presetUrl, scratchUrl, userAvatarUrl } from "@/lib/api/urls";
import getTranslation from "@/lib/i18n/translate";
import AnonymousFrogAvatar from "./user/AnonymousFrog";
import AnonymousFrogAvatar from "./Frog/AnonymousFrog";
import PlatformLink from "./PlatformLink";
import { calculateScorePercent, percentToString } from "./ScoreBadge";
import styles from "./ScratchItem.module.scss";

View File

@@ -1,27 +0,0 @@
.anonymousFrog {
/* Default: decomp.me purple */
--accent-hue: 298;
--accent-saturation: 0.75;
--accent-lightness: 0.4862;
--frog-pupil: #292f33;
--frog-primary:
hsl(
var(--accent-hue),
calc(100% * var(--accent-saturation)),
calc(100% * var(--accent-lightness))
);
/* Pure CSS doesn't have anything quite as slick as Sass' color.scale [as used in theme.scss]
for creating variations of a color. As such, we have to do a bit of the maths ourselves. */
--frog-secondary:
hsl(
var(--accent-hue),
calc(100% * (0.3 * (1 - var(--accent-saturation)) + var(--accent-saturation))), /* 30% towards maximum saturation */
calc(100% * (0.5 * (1 - var(--accent-lightness)) + var(--accent-lightness))) /* 50% towards maximum lightness */
);
--frog-nose:
hsl(
calc(100% * (-0.2 * (var(--accent-saturation)) + var(--accent-saturation))), /* 20% towards minimum saturation */
calc(100% * (-0.4 * (var(--accent-lightness)) + var(--accent-lightness))) /* 40% towards minimum lightness */
);
}

View File

@@ -9,7 +9,7 @@ import clsx from "clsx";
import * as api from "@/lib/api";
import { userAvatarUrl } from "@/lib/api/urls";
import AnonymousFrogAvatar from "./AnonymousFrog";
import AnonymousFrogAvatar from "../Frog/AnonymousFrog";
import styles from "./UserAvatar.module.scss";
export type Props = {