From 9fc12cb4e3eba96c49ee3aa1847d0e29fd5aa39b Mon Sep 17 00:00:00 2001 From: Micha de Vries Date: Wed, 27 Dec 2023 12:09:20 +0100 Subject: [PATCH] Navbar Actor selection (#242) * WIP actor in Navbar * Linting * Finish first implementation of actor selection * Fixup --- .../organisation/[organisation]/layout.tsx | 2 +- src/components/cards/avatar.tsx | 87 +++++--- src/components/cards/profile.tsx | 35 +++- src/components/layout/navbar.tsx | 198 ++++++++++++++---- src/schema/resources/actor.ts | 26 +++ 5 files changed, 275 insertions(+), 73 deletions(-) create mode 100644 src/schema/resources/actor.ts diff --git a/src/app/[locale]/(console)/organisation/[organisation]/layout.tsx b/src/app/[locale]/(console)/organisation/[organisation]/layout.tsx index 0fc3957..a3a2d7d 100644 --- a/src/app/[locale]/(console)/organisation/[organisation]/layout.tsx +++ b/src/app/[locale]/(console)/organisation/[organisation]/layout.tsx @@ -44,7 +44,7 @@ export default function ConsoleLayout({ children }: { children: ReactNode }) { return ( <> - + diff --git a/src/components/cards/avatar.tsx b/src/components/cards/avatar.tsx index 8508fa5..be0ff6a 100644 --- a/src/components/cards/avatar.tsx +++ b/src/components/cards/avatar.tsx @@ -1,7 +1,8 @@ import { cn } from '@/lib/utils'; import { Profile, unknownProfile } from '@/schema/resources/profile'; +import { Building, Users } from 'lucide-react'; import Image from 'next/image'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { AvatarFallback, AvatarImage, @@ -24,13 +25,14 @@ export function Avatar({ }: { profile?: Profile | null; loading?: boolean; - size?: 'tiny' | 'small' | 'normal' | 'big' | 'huge'; + size?: 'extra-tiny' | 'tiny' | 'small' | 'normal' | 'big' | 'huge'; renderBadge?: boolean; className?: string; }) { profile = profile ?? unknownProfile; const avatarFallback = avatarFallbackByName(profile.name); const avatarSize = { + 'extra-tiny': 'h-6 w-6 text-xs', tiny: 'h-8 w-8 text-md', small: 'h-10 w-10 text-lg', normal: 'h-12 w-12 text-xl', @@ -38,6 +40,34 @@ export function Avatar({ huge: 'h-20 w-20 text-4xl', }[size ?? 'normal']; + function Badge({ icon, text }: { icon: ReactNode; text: string }) { + return ( + + + +
+ {icon} +
+
+ +

{text}

+
+
+
+ ); + } + return loading ? ( - {renderBadge && 'type' in profile && profile.type == 'admin' && ( - - - -
- Playrbase Logo -
-
- -

Platform admin

-
-
-
- )} + {renderBadge && 'type' in profile ? ( + profile.type == 'admin' ? ( + + } + /> + ) : profile.type == 'organisation' ? ( + } + /> + ) : profile.type == 'team' ? ( + } + /> + ) : undefined + ) : undefined} ); } diff --git a/src/components/cards/profile.tsx b/src/components/cards/profile.tsx index 6b0d303..a5df444 100644 --- a/src/components/cards/profile.tsx +++ b/src/components/cards/profile.tsx @@ -16,7 +16,7 @@ export function Profile({ }: { profile?: TProfile | null; loading?: boolean; - size?: 'tiny' | 'small' | 'normal' | 'big'; + size?: 'extra-tiny' | 'tiny' | 'small' | 'normal' | 'big'; noSub?: boolean; customSub?: ReactNode | string; }) { @@ -27,20 +27,43 @@ export function Profile({
{loading ? ( -
- - +
+ {size == 'tiny' || size == 'extra-tiny' ? ( + <> + + {!noSub && } + + ) : ( + <> + + {!noSub && } + + )}
) : ( -
+

diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index 9dfe1a3..20cba0e 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -9,13 +9,25 @@ import { NavigationMenuTrigger, navigationMenuTriggerStyle, } from '@/components/ui/navigation-menu.tsx'; +import { useSurreal } from '@/lib/Surreal.tsx'; import { useAuth } from '@/lib/auth'; import { useFeatureFlags } from '@/lib/featureFlags.tsx'; import { useScrolledContext, useScrolledState } from '@/lib/scrolled.tsx'; import { cn } from '@/lib/utils.ts'; import { Language, languageEntries } from '@/locales/languages.ts'; import { Link, usePathname } from '@/locales/navigation.ts'; -import { ChevronRightSquare, Languages, LogOut, Menu } from 'lucide-react'; +import { Actor, linkToActorOverview } from '@/schema/resources/actor.ts'; +import { Organisation } from '@/schema/resources/organisation.ts'; +import { Team } from '@/schema/resources/team.ts'; +import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'; +import { useQuery } from '@tanstack/react-query'; +import { + ChevronRightSquare, + ChevronsUpDown, + Languages, + // LogOut, + Menu, +} from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import Image from 'next/image'; import React, { @@ -29,6 +41,7 @@ import React, { } from 'react'; import ReactCountryFlag from 'react-country-flag'; import * as portals from 'react-reverse-portal'; +import { z } from 'zod'; import LogoFull from '../../assets/LogoFull.svg'; import { Profile } from '../cards/profile.tsx'; import { Button } from '../ui/button.tsx'; @@ -61,7 +74,13 @@ export function NavbarProvider({ children }: { children?: ReactNode }) { ); } -export function Navbar({ children }: { children?: ReactNode }) { +export function Navbar({ + children, + actor, +}: { + children?: ReactNode; + actor?: Actor; +}) { const [onClient, setOnClient] = useState(false); const portal = useContext(NavbarContext); @@ -70,10 +89,20 @@ export function Navbar({ children }: { children?: ReactNode }) { }, []); if (!portal || !onClient) return null; - return {children}; + return ( + + {children} + + ); } -export const RenderNavbar = ({ children }: { children?: ReactNode }) => { +export const RenderNavbar = ({ + children, + actor, +}: { + children?: ReactNode; + actor?: Actor; +}) => { const scrolled = useScrolledState(); const [featureFlags] = useFeatureFlags(); const [open, setOpen] = useState(false); @@ -82,7 +111,13 @@ export const RenderNavbar = ({ children }: { children?: ReactNode }) => { // localStorage.setItem('playrbase_fflag_devTools', 'true') // Then reload page - const links = ; + const links = ( + + ); return (
{ const Links = ({ devTools, + actor, setOpen, }: { devTools: boolean; + actor?: Actor; setOpen: (open: boolean) => void; }) => { const { loading, user } = useAuth(); @@ -182,20 +219,33 @@ const Links = ({ onValueChange={setState} > - + {loading ? ( - +
+ + +
) : user ? ( <> - - - {user.name.split(' ')[0]} - +
+ + + + + + +
- + ) : ( @@ -234,35 +284,109 @@ const Links = ({ ); }; -const AccountOptions = () => { - const { user, loading, signout } = useAuth(); - const t = useTranslations('components.layout.navbar.account-options'); +const AccountOptions = ({ actor }: { actor?: Actor }) => { + // const t = useTranslations('components.layout.navbar.account-options'); + const surreal = useSurreal(); + const { user, loading: userLoading } = useAuth(); + const { data, isLoading: dataLoading } = useQuery({ + queryKey: ['navbar-actor-switcher'], + queryFn: async function () { + const [orgs, teams] = await surreal.query< + [Organisation[], Team[]] + >(/* surrealql */ ` + SELECT * FROM organisation WHERE part_of = NONE LIMIT 3; + SELECT * FROM team LIMIT 3; + `); + + const data = { + organisations: z.array(Organisation).parse(orgs), + teams: z.array(Team).parse(teams), + }; + + if (actor) { + if (actor.type == 'organisation') { + data.organisations = data.organisations.filter( + ({ id }) => id != actor.id + ); + data.organisations.unshift(actor as Organisation); + if (data.organisations.length > 3) data.organisations.pop(); + } + + if (actor.type == 'team') { + data.teams = data.teams.filter(({ id }) => id != actor.id); + data.teams.unshift(actor as Team); + if (data.teams.length > 3) data.teams.pop(); + } + } + + return data; + }, + }); + + const loading = userLoading || dataLoading; + if (!loading && (!data || !user)) return

An error occurred

; + const organisations = data?.organisations ?? []; + const teams = data?.teams ?? []; + + function LinkActor({ actor: thisActor }: { actor: Actor }) { + return ( + + + + + + + + ); + } return ( user && ( -
    - - - - - - - - +
      +
      + + {organisations.length > 0 && ( +
      + + Organisations + {' '} + {organisations.map((org) => ( + + ))} +
      + )} + {teams.length > 0 && ( +
      + + Teams + {' '} + {teams.map((team) => ( + + ))} +
      + )} +
      + {/* - + */}
    ) ); diff --git a/src/schema/resources/actor.ts b/src/schema/resources/actor.ts new file mode 100644 index 0000000..5f2375f --- /dev/null +++ b/src/schema/resources/actor.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; +import { Admin } from './admin'; +import { Organisation, OrganisationSafeParse } from './organisation'; +import { Team } from './team'; +import { User } from './user'; + +export const Actor = z.union([ + Admin, + User, + Organisation, + OrganisationSafeParse, + Team, +]); + +export type Actor = z.infer; + +export function linkToActorOverview(actor: Actor) { + switch (actor.type) { + case 'user': + return `/account`; + case 'organisation': + return `/organisation/${actor.slug}/overview`; + default: + return '/'; + } +}