Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ TINYAUTH_LDAP_ADDRESS=
TINYAUTH_LDAP_BINDDN=
# Bind password for LDAP authentication.
TINYAUTH_LDAP_BINDPASSWORD=
# Path to the Bind password.
TINYAUTH_LDAP_BINDPASSWORDFILE=
# Base DN for LDAP searches.
TINYAUTH_LDAP_BASEDN=
# Allow insecure LDAP connections.
Expand Down
36 changes: 0 additions & 36 deletions frontend/src/components/language/language.tsx

This file was deleted.

8 changes: 3 additions & 5 deletions frontend/src/components/layout/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useAppContext } from "@/context/app-context";
import { LanguageSelector } from "../language/language";
import { Outlet } from "react-router";
import { useCallback, useEffect, useState } from "react";
import { DomainWarning } from "../domain-warning/domain-warning";
import { ThemeToggle } from "../theme-toggle/theme-toggle";
import { QuickActions } from "../quick-actions/quick-actions";

const BaseLayout = ({ children }: { children: React.ReactNode }) => {
const { ui } = useAppContext();
Expand All @@ -21,9 +20,8 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
backgroundPosition: "center",
}}
>
<div className="absolute top-4 right-4 flex flex-row gap-2">
<ThemeToggle />
<LanguageSelector />
<div className="absolute top-4 right-4">
<QuickActions />
</div>
<div className="max-w-sm md:min-w-sm min-w-xs">{children}</div>
</div>
Expand Down
208 changes: 208 additions & 0 deletions frontend/src/components/quick-actions/quick-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { languages, SupportedLanguage } from "@/lib/i18n/locales";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { useState } from "react";
import i18n from "@/lib/i18n/i18n";
import { useUserContext } from "@/context/user-context";
import { ScrollArea } from "../ui/scroll-area";
import { useTheme } from "../providers/theme-provider";
import {
Check,
DoorOpenIcon,
Languages,
Monitor,
Moon,
Palette,
Settings,
Sun,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import { useRef } from "react";
import {
useScreenParams,
recompileScreenParams,
} from "@/lib/hooks/screen-params";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "sonner";
import { useEffect } from "react";

function Avatar({ initial }: { initial: string }) {
return (
<span className="group relative grid size-10 place-items-center rounded-full">
<span className="absolute inset-0 overflow-hidden rounded-full bg-linear-to-b from-neutral-50 to-neutral-100 dark:from-neutral-700 dark:to-neutral-950 shadow-lg"></span>
<span className="relative text-sm font-semibold text-primary">
{initial}
</span>
</span>
);
}

export const QuickActions = () => {
const { auth } = useUserContext();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const { search } = useLocation();

const [language, setLanguage] = useState<SupportedLanguage>(
i18n.language as SupportedLanguage,
);

const redirectTimer = useRef<number | null>(null);
const searchParams = new URLSearchParams(search);
const screenParams = useScreenParams(searchParams);
const compiledParams = recompileScreenParams(screenParams);

const logoutMutation = useMutation({
mutationFn: () => axios.post("/api/user/logout"),
mutationKey: ["logout"],
onSuccess: () => {
toast.success(t("logoutSuccessTitle"), {
description: t("logoutSuccessSubtitle"),
});

redirectTimer.current = window.setTimeout(() => {
window.location.replace(`/login${compiledParams}`);
}, 500);
},
onError: () => {
toast.error(t("logoutFailTitle"), {
description: t("logoutFailSubtitle"),
});
},
});

useEffect(() => {
return () => {
if (redirectTimer.current) {
clearTimeout(redirectTimer.current);
}
};
}, [redirectTimer]);

const initial = auth.authenticated
? (auth.name[0] || "U").toUpperCase()
: null;

const handleSelect = (option: string) => {
setLanguage(option as SupportedLanguage);
i18n.changeLanguage(option as SupportedLanguage);
};

const themes = [
{ key: "light", label: t("quickActionsThemeLight"), icon: Sun },
{ key: "dark", label: t("quickActionsThemeDark"), icon: Moon },
{ key: "system", label: t("quickActionsThemeSystem"), icon: Monitor },
] as const;

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
aria-label={t("quickActionsTitle")}
className="rounded-full transition-transform duration-200 will-change-transform hover:scale-105 hover:cursor-pointer focus:ring-0 focus:outline-3 focus:outline-ring/50"
>
{auth.authenticated ? (
<Avatar initial={initial!} />
) : (
<span className="bg-card text-primary border-border size-10 flex items-center justify-center rounded-full border shadow-lg">
<Settings className="size-4" />
</span>
)}
</button>
</DropdownMenuTrigger>

<DropdownMenuContent
align="end"
sideOffset={8}
className="rounded-xl p-1"
>
{auth.authenticated && (
<>
<DropdownMenuLabel className="flex items-center gap-3 p-2">
<div className="bg-foreground text-background flex size-9 shrink-0 items-center justify-center rounded-full text-sm font-medium">
{initial}
</div>
<div className="flex min-w-0 flex-col">
<span className="truncate text-sm font-medium">
{auth.name}
</span>
<span className="text-muted-foreground truncate text-xs font-normal">
{auth.email}
</span>
</div>
</DropdownMenuLabel>

<DropdownMenuSeparator />
</>
)}

<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Languages className="size-4" />
{t("quickActionsLanguage")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent sideOffset={8} className="rounded-xl p-1">
<ScrollArea className="h-80">
{Object.entries(languages).map(([key, value]) => (
<DropdownMenuItem
key={key}
onSelect={() => handleSelect(key)}
>
{value}
{language === key && <Check className="size-4" />}
</DropdownMenuItem>
))}
</ScrollArea>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>

<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Palette className="size-4" />
{t("quickActionsTheme")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="rounded-xl p-1" sideOffset={8}>
{themes.map(({ key, label, icon: Icon }) => (
<DropdownMenuItem key={key} onClick={() => setTheme(key)}>
<span className="flex items-center gap-2">
<Icon className="size-4" />
{label}
</span>
{theme === key && <Check className="size-4" />}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>

{auth.authenticated && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => logoutMutation.mutate()}
className="text-destructive"
>
<DoorOpenIcon className="size-4" />
{t("quickActionsLogout")}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
40 changes: 0 additions & 40 deletions frontend/src/components/theme-toggle/theme-toggle.tsx

This file was deleted.

56 changes: 56 additions & 0 deletions frontend/src/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"

function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}

function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}

export { ScrollArea, ScrollBar }
17 changes: 17 additions & 0 deletions frontend/src/lib/hooks/login-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type UseLoginForProps = {
login_for?: "oidc" | "app";
compiledParams: string;
};

export const useLoginFor = (props: UseLoginForProps): string => {
const { login_for, compiledParams } = props;

switch (login_for) {
case "oidc":
return "/oidc/authorize" + compiledParams;
case "app":
return "/continue" + compiledParams;
default:
return "/logout";
}
};
Loading