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
127 changes: 127 additions & 0 deletions src/components/NavBar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
.special-navbar-link {
--special-primary: var(--color-primary);
--special-primary-light: color-mix(in srgb, var(--special-primary) 83%, white);
--special-primary-mid: color-mix(in srgb, var(--special-primary) 92%, white);
--special-primary-dark: color-mix(in srgb, var(--special-primary) 85%, black);
--special-primary-deep: color-mix(in srgb, var(--special-primary) 72%, black);
--special-gradient-duration: 4s;
--special-glow-duration: 2.4s;
--special-hover-duration: 1000ms;
--special-transition-duration: 360ms;
--special-background-transition-duration: calc(
var(--special-transition-duration) -
40ms
);
--special-reduced-transition-duration: calc(
var(--special-transition-duration) /
2
);
--special-gradient-start: 0% 35%;
--special-gradient-end: 100% 65%;
position: relative;
overflow: hidden;
isolation: isolate;
background: linear-gradient(
115deg,
var(--special-primary-deep),
var(--special-primary-dark),
var(--special-primary),
var(--special-primary-mid),
var(--special-primary-dark),
var(--special-primary-light),
var(--special-primary-mid),
var(--special-primary)
);
background-size: 300% 300%;

color: var(--color-primary-foreground);
box-shadow: 0 0 0 1px
color-mix(in srgb, var(--special-primary) 55%, transparent), 0 0 12px
color-mix(in srgb, var(--special-primary) 30%, transparent);
animation: special-navbar-gradient var(--special-gradient-duration)
ease-in-out infinite alternate, special-navbar-glow
var(--special-glow-duration) ease-in-out infinite;
transition: background-position var(--special-background-transition-duration)
ease, box-shadow var(--special-transition-duration) ease, filter
var(--special-transition-duration) ease, transform
var(--special-transition-duration) cubic-bezier(0.22, 1, 0.36, 1);
will-change: background-position, transform;
}

.special-navbar-link:hover {
background-position: 100% 50%;
filter: brightness(1.06) saturate(1.1);
transform: translateY(-1px) scale(1.015);
animation: special-navbar-hover var(--special-hover-duration)
cubic-bezier(0.22, 1, 0.36, 1) both;
box-shadow: 0 0 0 1px
color-mix(in srgb, var(--special-primary-light) 65%, transparent), 0 2px 4px
color-mix(in srgb, var(--special-primary-deep) 30%, transparent), 0 0 18px
color-mix(in srgb, var(--special-primary) 45%, transparent);
}

.special-navbar-link:active {
transform: translateY(0) scale(0.985);
}

.special-navbar-phase-1 {
animation-delay: calc(var(--special-gradient-duration) * -0.25),
calc(var(--special-glow-duration) * -0.25);
}

.special-navbar-phase-2 {
animation-delay: calc(var(--special-gradient-duration) * -0.5),
calc(var(--special-glow-duration) * -0.5);
}

.special-navbar-phase-3 {
animation-delay: calc(var(--special-gradient-duration) * -0.75),
calc(var(--special-glow-duration) * -0.75);
}

@keyframes special-navbar-gradient {
0% {
background-position: var(--special-gradient-start);
}
100% {
background-position: var(--special-gradient-end);
}
}

@keyframes special-navbar-hover {
0% {
background-position: var(--special-gradient-start);
}
100% {
background-position: var(--special-gradient-end);
}
}

@keyframes special-navbar-glow {
0%,
100% {
box-shadow: 0 0 0 1px
color-mix(in srgb, var(--special-primary) 55%, transparent), 0 0 12px
color-mix(in srgb, var(--special-primary) 30%, transparent);
}
50% {
box-shadow: 0 0 0 1px
color-mix(in srgb, var(--special-primary-light) 72%, transparent), 0 0
20px color-mix(in srgb, var(--special-primary) 45%, transparent);
}
}

@media (prefers-reduced-motion: reduce) {
.special-navbar-link {
animation: none;
transition: background-position var(--special-reduced-transition-duration)
ease, box-shadow var(--special-reduced-transition-duration) ease, filter
var(--special-reduced-transition-duration) ease;
}

.special-navbar-link:hover {
background-position: 100% 50%;
transform: none;
animation: none;
}
}
129 changes: 93 additions & 36 deletions src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getMeOptions,
authCookieLogoutMutation,
getGuildMeetingOptions,
getVisibleElectionOptions,
} from "@/api/@tanstack/react-query.gen";
import {
DropdownMenu,
Expand All @@ -52,19 +53,22 @@ import { toast } from "sonner";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import VerificationReminder from "./VerificationReminder";
import MemberBanner from "./MemberBanner";
import { useAuthState } from "@/lib/auth";
import "./NavBar.css";

type NavItem = {
self: string;
desc: string;
href?: string;
special?: boolean;
};

type NavSection = {
self: string | NavItem;
} & Record<string, NavItem>;

export function NavBar() {
const { t } = useTranslation();
const { t } = useTranslation("main");
const router = useRouter();
const { data: user } = useQuery({
...getMeOptions(),
Expand All @@ -89,8 +93,7 @@ export function NavBar() {
logoutMutation.mutate({});
}, [logoutMutation]);

const showAdmin =
user?.is_member && Array.isArray(user.posts) && user.posts.length > 0;
const showAdmin = useAuthState().getPermissions().size > 0;

return (
<header className="sticky top-0 z-50 w-full border-transparent bg-white/50 dark:bg-background/40 backdrop-blur-md">
Expand Down Expand Up @@ -292,7 +295,7 @@ function useLoginHandler() {
}

export function NavBarMenu({ isMobile = false }: { isMobile?: boolean }) {
const { t } = useTranslation();
const { t, i18n } = useTranslation("main");
const navbarData = t("navbar", { returnObjects: true }) as Record<
string,
NavSection
Expand All @@ -313,9 +316,41 @@ export function NavBarMenu({ isMobile = false }: { isMobile?: boolean }) {
"guildMeeting",
{
self: {
self: t("navbar.guild-meeting"),
self: t("navbar.guild-meeting-now", {
title:
i18n.language === "en"
? guildMeetingData.title_en
: guildMeetingData.title_sv,
}),

desc: "",
href: "/guild-meeting",
special: true,
},
},
]);
}

const { data: electionData } = useQuery({
...getVisibleElectionOptions(),
refetchOnWindowFocus: false,
});

if (electionData?.visible) {
sections.push([
"election",
{
self: {
self: t("navbar.election-now", {
title:
i18n.language === "en"
? electionData.title_en
: electionData.title_sv,
}),

desc: "",
href: "/election",
special: true,
},
},
]);
Expand All @@ -331,26 +366,19 @@ export function NavBarMenu({ isMobile = false }: { isMobile?: boolean }) {
// Mobile vertical layout
return (
<div className="space-y-4">
{sections.map(([sectionKey, section]) => {
{sections.map(([sectionKey, section], sectionIndex) => {
// Standalone item
if (isStandaloneItem(section)) {
const item = section.self;
return (
<div key={sectionKey} className="space-y-2">
<SheetClose asChild>
<Link
href={item.href || "#"}
className={cn(
"flex items-center gap-2 px-2 py-2 text-sm rounded-md transition-colors hover:bg-accent font-medium",
(!item.href || item.href === "#") &&
"opacity-50 cursor-not-allowed pointer-events-none",
)}
>
<span>{item.self}</span>
{item.href?.startsWith("https://") && (
<ExternalLink className="w-4 h-4" />
)}
</Link>
<NavBarStandaloneLink
className="flex items-center"
item={item}
isMobile={isMobile}
animationIndex={sectionIndex}
/>
</SheetClose>
</div>
);
Expand Down Expand Up @@ -404,7 +432,7 @@ export function NavBarMenu({ isMobile = false }: { isMobile?: boolean }) {
custom-navmenu
"
>
{sections.map(([sectionKey, section]) => {
{sections.map(([sectionKey, section], sectionIndex) => {
// Standalone item support for desktop
if (isStandaloneItem(section)) {
const item = section.self;
Expand All @@ -415,22 +443,12 @@ export function NavBarMenu({ isMobile = false }: { isMobile?: boolean }) {
>
<NavigationMenuItem className="bg-transparent hover:bg-transparent">
<NavigationMenuLink asChild>
<Link
href={item.href || "#"}
className={cn(
"submenu-trigger !bg-transparent !hover:bg-transparent border-2 border-transparent hover:border-foreground/30 font-medium px-4 py-2",
(!item.href || item.href === "#") &&
"opacity-50 cursor-not-allowed pointer-events-none",
)}
>
{item.self}
{item.href?.startsWith("https://") && (
<ExternalLink
className="inline w-6 h-6 ml-1"
aria-label="External link"
/>
)}
</Link>
<NavBarStandaloneLink
className="submenu-trigger"
item={item}
isMobile={isMobile}
animationIndex={sectionIndex}
/>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
Expand Down Expand Up @@ -511,3 +529,42 @@ const ListItem = React.forwardRef<
);
});
ListItem.displayName = "ListItem";

function NavBarStandaloneLink({
className,
item,
isMobile = false,
animationIndex = 0,
...props
}: {
item: NavItem;
isMobile?: boolean;
className?: string;
animationIndex?: number;
}) {
return (
<Link
href={item.href || "#"}
className={cn(
className,
"font-medium whitespace-nowrap",
isMobile
? "gap-2 px-2 py-2 text-sm rounded-md transition-colors hover:bg-accent"
: "h-9 px-4 py-0 ml-4 flex justify-center bg-transparent hover:bg-transparent border-2 border-transparent hover:border-foreground/30",
(!item.href || item.href === "#") &&
"opacity-50 cursor-not-allowed pointer-events-none",
item.special &&
`special-navbar-link special-navbar-phase-${animationIndex % 4} border-none hover:border-none`,
)}
{...props}
>
{item.self}
{item.href?.startsWith("https://") && (
<ExternalLink
className="inline w-6 h-6 ml-1"
aria-label="External link"
/>
)}
</Link>
);
}
4 changes: 3 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@
"logout": "Log out",
"logoutError": "Logout failed. Please try again later.",
"verify": "Verify Email",
"guild-meeting": "Guild Meeting"
"guild-meeting": "Guild Meeting",
"guild-meeting-now": "{{title}}",
"election-now": "{{title}}"
},
"footer": {
"contact": "Contact",
Expand Down
4 changes: 3 additions & 1 deletion src/locales/sv/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@
"logout": "Logga ut",
"logoutError": "Utloggning misslyckades. Försök igen senare.",
"verify": "Verifiera e-post",
"guild-meeting": "Sektionsmöte"
"guild-meeting": "Sektionsmöte",
"guild-meeting-now": "{{title}}",
"election-now": "{{title}}"
},
"footer": {
"contact": "Kontakt",
Expand Down