Skip to content

Commit 0bc2a66

Browse files
authored
Update grades navbar (#3073)
1 parent b9ed6c7 commit 0bc2a66

File tree

12 files changed

+359
-38
lines changed

12 files changed

+359
-38
lines changed
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
{}
1+
{
2+
"Navbar": {
3+
"courses": "Courses",
4+
"theme": "Theme",
5+
"language": "Language",
6+
"norwegian": "Norsk",
7+
"english": "English",
8+
"courseSearchPlaceholder": "Search for courses..."
9+
},
10+
"ThemePopover": {
11+
"light": "Light",
12+
"dark": "Dark",
13+
"system": "System"
14+
}
15+
}
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
{}
1+
{
2+
"Navbar": {
3+
"courses": "Emner",
4+
"theme": "Fargetema",
5+
"language": "Språk",
6+
"norwegian": "Norsk",
7+
"english": "English",
8+
"courseSearchPlaceholder": "Søk etter emner..."
9+
},
10+
"ThemePopover": {
11+
"light": "Lys",
12+
"dark": "Mørk",
13+
"system": "System"
14+
}
15+
}

apps/grades-frontend/src/app/Navbar.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { cn, TextInput } from "@dotkomonline/ui"
2+
import { IconSearch } from "@tabler/icons-react"
3+
4+
interface Props {
5+
className?: string
6+
placeholder?: string
7+
}
8+
9+
export const CourseSearch = ({ className, placeholder }: Props) => {
10+
return (
11+
<div className={cn("relative", className)}>
12+
<IconSearch className="w-8 h-full pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3" />
13+
<TextInput className="pl-10 rounded-lg w-full h-full dark:border-none text-base" placeholder={placeholder} />
14+
</div>
15+
)
16+
}
File renamed without changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client"
2+
3+
import { setLocale } from "@/i18n/set-locale"
4+
import { Popover, PopoverContent, PopoverTrigger, Text } from "@dotkomonline/ui"
5+
import { IconWorld } from "@tabler/icons-react"
6+
import { useLocale, useTranslations } from "next-intl"
7+
import { useState } from "react"
8+
import { PopoverOptionButton } from "./PopoverOptionButton"
9+
10+
export const LocalePopover = () => {
11+
const locale = useLocale()
12+
const t = useTranslations("Navbar")
13+
14+
const [languagePopoverOpen, setLanguagePopoverOpen] = useState(false)
15+
16+
const onLocaleChange = (newLocale: "no" | "en") => {
17+
if (newLocale === locale) {
18+
return
19+
}
20+
21+
setLocale(newLocale)
22+
setLanguagePopoverOpen(false)
23+
}
24+
25+
const currentLanguage = locale === "no" ? t("norwegian") : t("english")
26+
27+
return (
28+
<Popover open={languagePopoverOpen} onOpenChange={setLanguagePopoverOpen}>
29+
<PopoverTrigger className="flex items-center justify-center rounded-lg bg-transparent p-2 text-neutral-800 hover:bg-neutral-100 gap-2">
30+
<IconWorld size={20} stroke={1.8} />
31+
{currentLanguage}
32+
</PopoverTrigger>
33+
<PopoverContent className="flex flex-col p-1 min-w-30">
34+
<PopoverOptionButton onClick={() => onLocaleChange("no")} isActive={locale === "no"}>
35+
<Text>{t("norwegian")}</Text>
36+
</PopoverOptionButton>
37+
<PopoverOptionButton onClick={() => onLocaleChange("en")} isActive={locale === "en"}>
38+
<Text>{t("english")}</Text>
39+
</PopoverOptionButton>
40+
</PopoverContent>
41+
</Popover>
42+
)
43+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client"
2+
3+
import type { Locale } from "@/i18n/locale"
4+
import { setLocale } from "@/i18n/set-locale"
5+
import {
6+
Button,
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuSeparator,
10+
DropdownMenuTrigger,
11+
Text,
12+
cn,
13+
} from "@dotkomonline/ui"
14+
import { IconDeviceMobile, IconMenu2, IconMoon, IconPalette, IconSun, IconWorld } from "@tabler/icons-react"
15+
import { useLocale, useTranslations } from "next-intl"
16+
import { useTheme } from "next-themes"
17+
import Link from "next/link"
18+
import { usePathname } from "next/navigation"
19+
import { useState } from "react"
20+
21+
export const MobileNavigation = () => {
22+
const t = useTranslations("Navbar")
23+
const pathname = usePathname()
24+
const locale = useLocale()
25+
const { theme, setTheme } = useTheme()
26+
const [isOpen, setIsOpen] = useState(false)
27+
28+
const onThemeChange = (newTheme: "light" | "dark" | "system") => {
29+
if (newTheme === theme) {
30+
return
31+
}
32+
33+
setTheme(newTheme)
34+
}
35+
36+
const onLocaleChange = (newLocale: Locale) => {
37+
if (newLocale === locale) {
38+
return
39+
}
40+
41+
setLocale(newLocale)
42+
}
43+
44+
return (
45+
<div className="sm:hidden">
46+
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
47+
<DropdownMenuTrigger className="flex items-center">
48+
<IconMenu2 />
49+
</DropdownMenuTrigger>
50+
51+
<DropdownMenuContent
52+
align="start"
53+
side="bottom"
54+
sideOffset={8}
55+
className="sm:hidden w-[calc(100vw-2rem)] mx-4 mt-3 z-50 shadow-sm animate-none! border-neutral-200 p-4"
56+
>
57+
<nav className="max-h-[calc(100dvh-8rem)] overflow-y-auto">
58+
<div className="space-y-5">
59+
<section>
60+
<Link
61+
href="/emner"
62+
onClick={() => setIsOpen(false)}
63+
className={cn(
64+
"flex items-center rounded-md px-3 py-2 font-medium transition-colors gap-2",
65+
pathname === "/emner"
66+
? "bg-neutral-100 text-neutral-900"
67+
: "text-neutral-700 hover:bg-neutral-50 hover:text-neutral-900"
68+
)}
69+
>
70+
{t("courses")}
71+
</Link>
72+
73+
<DropdownMenuSeparator className="my-2 bg-gray-300 dark:bg-stone-700" />
74+
</section>
75+
76+
<section className="flex flex-col gap-4 ml-3">
77+
<div className="flex flex-row gap-5">
78+
<Text className="flex items-center gap-1.5 text-sm text-neutral-700">
79+
<IconPalette size={16} />
80+
{t("theme")}
81+
</Text>
82+
<div className="flex flex-row gap-3">
83+
<ToggleButton onClick={() => onThemeChange("light")} isActive={theme === "light"}>
84+
<IconSun size={16} />
85+
</ToggleButton>
86+
<ToggleButton onClick={() => onThemeChange("dark")} isActive={theme === "dark"}>
87+
<IconMoon size={16} />
88+
</ToggleButton>
89+
<ToggleButton onClick={() => onThemeChange("system")} isActive={theme === "system"}>
90+
<IconDeviceMobile size={16} />
91+
</ToggleButton>
92+
</div>
93+
</div>
94+
<div className="flex flex-row gap-5">
95+
<Text className="flex items-center gap-1.5 text-sm text-neutral-700">
96+
<IconWorld size={16} />
97+
{t("language")}
98+
</Text>
99+
<div className="flex flex-row gap-3">
100+
<ToggleButton onClick={() => onLocaleChange("no")} isActive={locale === "no"}>
101+
{t("norwegian")}
102+
</ToggleButton>
103+
<ToggleButton onClick={() => onLocaleChange("en")} isActive={locale === "en"}>
104+
{t("english")}
105+
</ToggleButton>
106+
</div>
107+
</div>
108+
</section>
109+
</div>
110+
</nav>
111+
</DropdownMenuContent>
112+
</DropdownMenu>
113+
</div>
114+
)
115+
}
116+
117+
interface ToggleButtonProps {
118+
isActive?: boolean
119+
onClick: () => void
120+
children: React.ReactNode
121+
}
122+
123+
const ToggleButton = ({ isActive, onClick, children }: ToggleButtonProps) => {
124+
return (
125+
<Button
126+
variant="text"
127+
onClick={onClick}
128+
className={cn(
129+
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-stone-600 hover:text-neutral-900",
130+
isActive && "font-medium bg-neutral-100"
131+
)}
132+
>
133+
<span className="flex items-center gap-2">{children}</span>
134+
</Button>
135+
)
136+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client"
2+
3+
import { cn, Title } from "@dotkomonline/ui"
4+
import { useTranslations } from "next-intl"
5+
import Link from "next/link"
6+
import { usePathname } from "next/navigation"
7+
import { CourseSearch } from "../CourseSearch"
8+
import { LocalePopover } from "./LocalePopover"
9+
import { MobileNavigation } from "./MobileNavigation"
10+
import { ThemePopover } from "./ThemePopover"
11+
12+
export const Navbar = () => {
13+
const pathname = usePathname()
14+
const t = useTranslations("Navbar")
15+
16+
return (
17+
<header className="sticky top-0 z-50 w-full border-b border-neutral-200 bg-white/90 backdrop-blur-md">
18+
<div className="flex h-16 w-full items-center justify-between max-w-screen-xl mx-auto px-4 lg:px-12 gap-8">
19+
<div className="flex items-center gap-8 w-full">
20+
<Link href="/">
21+
<Title className="text-2xl font-bold leading-none text-black">Grades</Title>
22+
</Link>
23+
24+
<Link
25+
href="/emner"
26+
className={cn(
27+
"relative items-center rounded-lg px-3 py-1.5 text-[15px] font-medium transition-colors hidden sm:inline-flex",
28+
pathname === "/emner" ? "text-neutral-900" : "text-neutral-700 hover:text-neutral-900"
29+
)}
30+
>
31+
{t("courses")}
32+
{pathname === "/emner" && <span className="absolute bottom-0 left-3 right-3 h-0.5 rounded-full bg-black" />}
33+
</Link>
34+
35+
<CourseSearch placeholder={t("courseSearchPlaceholder")} className="h-9 w-full max-w-lg" />
36+
</div>
37+
38+
<div className="items-center gap-3 hidden sm:flex">
39+
<LocalePopover />
40+
<ThemePopover />
41+
</div>
42+
43+
<MobileNavigation />
44+
</div>
45+
</header>
46+
)
47+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Button, cn } from "@dotkomonline/ui"
2+
import { IconCheck } from "@tabler/icons-react"
3+
4+
interface Props {
5+
children: React.ReactNode
6+
isActive?: boolean
7+
onClick: () => void
8+
className?: string
9+
}
10+
11+
export const PopoverOptionButton = ({ children, isActive, onClick, className }: Props) => (
12+
<Button
13+
variant="text"
14+
onClick={onClick}
15+
className={cn(
16+
"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-100 dark:hover:bg-stone-600 hover:text-neutral-900",
17+
isActive && "font-medium",
18+
className
19+
)}
20+
>
21+
<span className="flex items-center gap-2">{children}</span>
22+
{isActive && <IconCheck size={16} stroke={2} />}
23+
</Button>
24+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client"
2+
3+
import { Popover, PopoverContent, PopoverTrigger, Text } from "@dotkomonline/ui"
4+
import { IconDeviceDesktop, IconDeviceMobile, IconMoon, IconSun } from "@tabler/icons-react"
5+
import { useTheme } from "next-themes"
6+
import { useState } from "react"
7+
import { PopoverOptionButton } from "./PopoverOptionButton"
8+
import { useTranslations } from "next-intl"
9+
10+
export const ThemePopover = () => {
11+
const { theme, setTheme, resolvedTheme } = useTheme()
12+
const [isOpen, setIsOpen] = useState(false)
13+
const t = useTranslations("ThemePopover")
14+
15+
const onChange = (newTheme: string) => {
16+
if (newTheme === theme) {
17+
return
18+
}
19+
20+
setIsOpen(false)
21+
setTheme(newTheme)
22+
}
23+
24+
return (
25+
<Popover open={isOpen} onOpenChange={setIsOpen}>
26+
<PopoverTrigger className="flex size-9 items-center justify-center rounded-lg bg-transparent p-0 text-neutral-800 hover:bg-neutral-100">
27+
{resolvedTheme === "light" ? <IconSun size={20} stroke={1.8} /> : <IconMoon size={20} stroke={1.8} />}
28+
</PopoverTrigger>
29+
<PopoverContent className="min-w-36 flex flex-col p-1 transition-colors">
30+
<PopoverOptionButton onClick={() => onChange("light")} isActive={theme === "light"}>
31+
<IconSun size={16} />
32+
<Text>{t("light")}</Text>
33+
</PopoverOptionButton>
34+
35+
<PopoverOptionButton onClick={() => onChange("dark")} isActive={theme === "dark"}>
36+
<IconMoon size={16} />
37+
<Text>{t("dark")}</Text>
38+
</PopoverOptionButton>
39+
40+
<PopoverOptionButton onClick={() => onChange("system")} isActive={theme === "system"}>
41+
<IconDeviceDesktop size={16} className="hidden xs:block" />
42+
<IconDeviceMobile size={16} className="xs:hidden" />
43+
<Text>{t("system")}</Text>
44+
</PopoverOptionButton>
45+
</PopoverContent>
46+
</Popover>
47+
)
48+
}

0 commit comments

Comments
 (0)