feat: apply new style on side-navigation component

This commit is contained in:
Sonny
2024-05-10 16:30:27 +02:00
committed by Sonny
parent 5f5eece627
commit b5cda75790
22 changed files with 193 additions and 624 deletions

View File

@@ -0,0 +1,9 @@
import styled from '@emotion/styled';
const TextEllipsis = styled.p({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
});
export default TextEllipsis;

View File

@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import VisibilityBadge from '~/components/dashboard/side_nav/visibilty/visibilty';
import VisibilityBadge from '~/components/visibilty/visibilty';
import useActiveCollection from '~/hooks/use_active_collection';
const CollectionNameWrapper = styled.div({

View File

@@ -9,7 +9,6 @@ const IMG_LOAD_TIMEOUT = 7_500;
interface LinkFaviconProps {
url: string;
size?: number;
noMargin?: boolean;
}
const Favicon = styled.div({
@@ -31,11 +30,7 @@ const FaviconLoader = styled.div(({ theme }) => ({
// The Favicon API should always return an image, so it's not really useful to keep the loader nor placeholder icon,
// but for slow connections and other random stuff, I'll keep this
export default function LinkFavicon({
url,
size = 32,
noMargin = false,
}: LinkFaviconProps) {
export default function LinkFavicon({ url, size = 32 }: LinkFaviconProps) {
const imgRef = useRef<HTMLImageElement>(null);
const [isFailed, setFailed] = useState<boolean>(false);
@@ -60,7 +55,7 @@ export default function LinkFavicon({
}, [isLoading]);
return (
<Favicon style={{ marginRight: !noMargin ? '1em' : '0' }}>
<Favicon>
{!isFailed ? (
<img
src={`/favicon?url=${url}`}

View File

@@ -1,223 +0,0 @@
import PATHS from '#constants/paths';
import type Collection from '#models/collection';
import styled from '@emotion/styled';
import { useCallback, useEffect, useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { useTranslation } from 'react-i18next';
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
import VisibilityBadge from '~/components/dashboard/side_nav/visibilty/visibilty';
import useActiveCollection from '~/hooks/use_active_collection';
import useCollections from '~/hooks/use_collections';
import { arrayMove } from '~/lib/array';
import sortCcollectionsByNextId from '~/lib/collection';
import { makeRequest } from '~/lib/request';
interface CollectionItemProps {
collection: Collection;
index: number;
}
type CollectionDragItem = {
collectionId: Collection['id'];
index: number;
};
const CollectionItemStyle = styled.li<{ active: boolean }>(
({ theme, active }) => ({
color: active ? theme.colors.blue : theme.colors.font,
borderBottom: active
? `'2px solid ${theme.colors.lightestGrey} !important'`
: '2px solid transparent !important',
display: 'flex',
gap: '0.25em',
alignItems: 'center',
justifyContent: 'space-between',
transition: `${theme.transition.delay} box-shadow`,
'&:hover': {
color: theme.colors.blue,
},
})
);
const CollectionContent = styled.div({
width: 'calc(100% - 24px)',
display: 'flex',
gap: '0.35em',
alignItems: 'center',
});
const CollectionNameWrapper = styled.div({
minWidth: 0,
width: '100%',
display: 'flex',
gap: '0.35em',
flex: 1,
alignItems: 'center',
});
const CollectionName = styled.div({
minWidth: 0,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
});
const CollectionLinksCount = styled.div(({ theme }) => ({
minWidth: 'fit-content',
fontSize: '0.85em',
color: theme.colors.grey,
}));
export default function CollectionItem({
collection,
index,
}: Readonly<CollectionItemProps>): JSX.Element {
const { activeCollection, setActiveCollection } = useActiveCollection();
const { collections, setCollections } = useCollections();
const { t } = useTranslation();
const ref = useRef<HTMLLIElement>();
const sendMoveCollectionRequest = useCallback(
async (currentCollection: Collection, nextId?: string) => {
if (currentCollection.id === nextId) return;
await makeRequest({
url: `${PATHS.API.COLLECTION}/${currentCollection.id}`,
method: 'PUT',
body: {
name: currentCollection.name,
nextId,
},
});
// @ts-ignore
setCollections((prevCollections) => {
const newCollections = [...prevCollections];
const collectionIndex = newCollections.findIndex(
(c) => c.id === currentCollection.id
);
const previousCollectionIndex = newCollections.findIndex(
(c) => c.nextId === currentCollection.id
);
const prevNextCollectionIndex = newCollections.findIndex(
(c) => c.nextId === nextId
);
newCollections[collectionIndex] = {
...newCollections[collectionIndex],
nextId,
};
if (previousCollectionIndex !== -1) {
newCollections[previousCollectionIndex] = {
...newCollections[previousCollectionIndex],
nextId: currentCollection.nextId,
};
}
if (prevNextCollectionIndex !== -1) {
newCollections[prevNextCollectionIndex] = {
...newCollections[prevNextCollectionIndex],
nextId: currentCollection.id,
};
}
return sortCcollectionsByNextId(newCollections);
});
},
[setCollections]
);
const moveCollection = useCallback(
(currentIndex: number, newIndex: number) => {
// @ts-ignore
setCollections((prevCollections: Collection[]) =>
arrayMove(prevCollections, currentIndex, newIndex)
);
},
[setCollections]
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const [_, drop] = useDrop({
accept: 'collection',
hover: (dragItem: CollectionDragItem) => {
if (ref.current && dragItem.collectionId !== collection.id) {
moveCollection(dragItem.index, index);
dragItem.index = index;
}
},
drop: (item) => {
const currentCollection = collections.find(
(c) => c.id === item.collectionId
);
const nextCollection = collections[item.index + 1];
if (
currentCollection?.nextId === null &&
nextCollection?.id === undefined
)
return;
if (currentCollection?.nextId !== nextCollection?.id) {
sendMoveCollectionRequest(
currentCollection!,
nextCollection?.id ?? null
);
}
},
});
const [{ opacity }, drag] = useDrag({
type: 'collection',
item: () => ({ index, collectionId: collection.id }),
collect: (monitor: any) => ({
opacity: monitor.isDragging() ? 0.1 : 1,
}),
end: (dragItem: CollectionDragItem, monitor) => {
const didDrop = monitor.didDrop();
if (!didDrop) {
moveCollection(dragItem.index, index);
}
},
});
useEffect(() => {
if (collection.id === activeCollection?.id) {
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [collection.id, activeCollection?.id]);
drag(drop(ref));
return (
<CollectionItemStyle
style={{
transition: 'none',
opacity,
}}
onClick={() => setActiveCollection(collection)}
title={collection.name}
active={collection.id === activeCollection?.id}
// @ts-ignore
ref={ref}
>
{collection.id === activeCollection?.id ? (
<AiFillFolderOpen size={24} />
) : (
<AiOutlineFolder size={24} />
)}
<CollectionContent>
<CollectionNameWrapper>
<CollectionName>{collection.name}</CollectionName>
<CollectionLinksCount>
{collection.links.length}
</CollectionLinksCount>
</CollectionNameWrapper>
<VisibilityBadge
label={t('common:collection.visibility')}
visibility={collection.visibility}
/>
</CollectionContent>
</CollectionItemStyle>
);
}

View File

@@ -1,99 +0,0 @@
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { DndProvider, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import UnstyledList from '~/components/common/unstyled/unstyled_list';
import CollectionItem from '~/components/dashboard/side_nav/collection/collection_item';
import useActiveCollection from '~/hooks/use_active_collection';
import useCollections from '~/hooks/use_collections';
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
import Keys from '../../../../../app/constants/keys';
const CollectionListStyle = styled.div({
height: '100%',
width: '100%',
minHeight: 0,
display: 'flex',
flex: 1,
flexDirection: 'column',
});
export default function CollectionList() {
const { t } = useTranslation();
const { collections } = useCollections();
const { activeCollection, setActiveCollection } = useActiveCollection();
const { globalHotkeysEnabled } = useGlobalHotkeys();
const linksCount = useMemo(
() =>
collections.reduce((acc, current) => (acc += current.links.length), 0),
[collections]
);
useHotkeys(
Keys.ARROW_UP,
() => {
const currentCollectionIndex = collections.findIndex(
({ id }) => id === activeCollection?.id
);
if (currentCollectionIndex === -1 || currentCollectionIndex === 0) return;
setActiveCollection(collections[currentCollectionIndex - 1]);
},
{ enabled: globalHotkeysEnabled }
);
useHotkeys(
Keys.ARROW_DOWN,
() => {
const currentCollectionIndex = collections.findIndex(
({ id }) => id === activeCollection?.id
);
if (
currentCollectionIndex === -1 ||
currentCollectionIndex === collections.length - 1
)
return;
setActiveCollection(collections[currentCollectionIndex + 1]);
},
{ enabled: globalHotkeysEnabled }
);
return (
<CollectionListStyle>
<h4>
{t('collection.collections', { count: linksCount })} {linksCount}
</h4>
<DndProvider backend={HTML5Backend}>
<ListOfCollections />
</DndProvider>
</CollectionListStyle>
);
}
function ListOfCollections() {
const [, drop] = useDrop(() => ({ accept: 'collection' }));
const { collections } = useCollections();
return (
<UnstyledList
css={{
overflowY: 'auto',
overflowX: 'hidden',
}}
// @ts-ignore
ref={drop}
>
{collections.map((collection, index) => (
<CollectionItem
collection={collection}
key={collection.id}
index={index}
/>
))}
</UnstyledList>
);
}

View File

@@ -0,0 +1,11 @@
import styled from '@emotion/styled';
const FavoriteListContainer = styled.div({
height: '100%',
minHeight: 0,
paddingInline: '10px',
display: 'flex',
flexDirection: 'column',
});
export default FavoriteListContainer;

View File

@@ -0,0 +1,8 @@
import styled from '@emotion/styled';
import { ItemLink } from '~/components/dashboard/side_nav/nav_item';
const FavoriteItem = styled(ItemLink)(({ theme }) => ({
backgroundColor: theme.colors.white,
}));
export default FavoriteItem;

View File

@@ -0,0 +1,49 @@
import styled from '@emotion/styled';
import TextEllipsis from '~/components/common/text_ellipsis';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
import FavoriteListContainer from '~/components/dashboard/side_nav/favorite/favorite_container';
import FavoriteItem from '~/components/dashboard/side_nav/favorite/favorite_item';
import useFavorites from '~/hooks/use_favorites';
const FavoriteLabel = styled.p(({ theme }) => ({
color: theme.colors.grey,
}));
const NoFavorite = () => (
<FavoriteLabel css={{ textAlign: 'center' }}>
Your favorites will appear here
</FavoriteLabel>
);
const FavoriteListStyle = styled.div({
padding: '1px',
paddingRight: '5px',
display: 'flex',
flex: 1,
gap: '.35em',
flexDirection: 'column',
overflow: 'auto',
});
export default function FavoriteList() {
const { favorites } = useFavorites();
if (favorites.length === 0) {
return <NoFavorite key="no-favorite" />;
}
return (
<FavoriteListContainer>
<FavoriteLabel css={{ marginBlock: '0.35em', paddingInline: '15px' }}>
Favorites {favorites.length}
</FavoriteLabel>
<FavoriteListStyle>
{favorites.map(({ id, name, url }) => (
<FavoriteItem href={url} key={id}>
<LinkFavicon url={url} size={24} />
<TextEllipsis>{name}</TextEllipsis>
</FavoriteItem>
))}
</FavoriteListStyle>
</FavoriteListContainer>
);
}

View File

@@ -1,67 +0,0 @@
import type Link from '#models/link';
import styled from '@emotion/styled';
import ExternalLink from '~/components/common/external_link';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
const FavoriteLinkStyle = styled.li(({ theme }) => ({
width: '100%',
backgroundColor: theme.colors.white,
padding: 0,
border: `1px solid ${theme.colors.lightestGrey}`,
borderBottom: `2px solid ${theme.colors.lightestGrey}`,
borderRadius: theme.border.radius,
transition: theme.transition.delay,
'&:hover': {
color: theme.colors.blue,
backgroundColor: theme.colors.lightGrey,
borderBottom: `2px solid ${theme.colors.blue}`,
},
}));
const LinkStyle = styled(ExternalLink)({
width: '100%',
color: 'inherit',
padding: '0.5em 1em',
border: '0 !important',
display: 'flex',
alignItems: 'center',
gap: '0.25em',
});
const LinkName = styled.span({
display: 'block',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
});
const CollectionName = styled.span(({ theme }) => ({
maxWidth: '75px',
fontSize: '0.85em',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: theme.colors.grey,
display: 'block',
overflow: 'hidden',
}));
const FavoriteItem = ({
link,
}: Readonly<{
link: Link;
}>) => (
<FavoriteLinkStyle>
<LinkStyle
href={link.url}
target="_blank"
rel="noreferrer"
title={link.name}
>
<LinkFavicon url={link.url} size={24} noMargin />
<LinkName>{link.name}</LinkName>
<CollectionName> - {link.collection.name}</CollectionName>
</LinkStyle>
</FavoriteLinkStyle>
);
export default FavoriteItem;

View File

@@ -1,44 +0,0 @@
import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next';
import UnstyledList from '~/components/common/unstyled/unstyled_list';
import FavoriteItem from '~/components/dashboard/side_nav/favorites/favorite_item';
import useFavorites from '~/hooks/use_favorites';
const FavoriteListWrapper = styled.div({
height: 'auto',
width: '100%',
marginBottom: '15px',
});
const FavoriteListLegend = styled.h4(({ theme }) => ({
userSelect: 'none',
textTransform: 'uppercase',
fontSize: '0.85em',
fontWeight: '500',
color: theme.colors.grey,
marginBottom: '5px',
}));
const FavoriteListStyle = styled(UnstyledList)({
display: 'flex',
gap: '5px',
flexDirection: 'column',
});
export default function FavoriteList() {
const { t } = useTranslation('common');
const { favorites } = useFavorites();
return (
favorites.length !== 0 && (
<FavoriteListWrapper>
<FavoriteListLegend>{t('favorite')}</FavoriteListLegend>
<FavoriteListStyle>
{favorites.map((link) => (
<FavoriteItem link={link} key={link.id} />
))}
</FavoriteListStyle>
</FavoriteListWrapper>
)
);
}

View File

@@ -0,0 +1,28 @@
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
export const Item = styled.div(({ theme }) => ({
userSelect: 'none',
height: '40px',
width: '280px',
backgroundColor: theme.colors.lightGrey,
padding: '8px 12px',
borderRadius: theme.border.radius,
display: 'flex',
gap: '.75em',
alignItems: 'center',
'& > svg': {
height: '24px',
width: '24px',
},
// Disable hover effect for UserCard
'&:hover:not(.disable-hover)': {
backgroundColor: theme.colors.white,
outlineWidth: '1px',
outlineStyle: 'solid',
},
}));
export const ItemLink = Item.withComponent(Link);

View File

@@ -1,46 +0,0 @@
import PATHS from '#constants/paths';
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import useActiveCollection from '~/hooks/use_active_collection';
const MenuControls = styled.div({
margin: '10px 0',
display: 'flex',
gap: '0.25em',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
});
const MenuAction = styled.div({
fontSize: '0.95em',
display: 'flex',
gap: '0.5em',
});
export default function NavigationLinks() {
const { t } = useTranslation('common');
const { activeCollection } = useActiveCollection();
return (
<MenuControls>
{/* <MenuAction>
<SearchModal>{t('common:search')}</SearchModal>
<kbd>S</kbd>
</MenuAction> */}
<MenuAction>
<Link href={PATHS.COLLECTION.CREATE}>{t('collection.create')}</Link>
<kbd>C</kbd>
</MenuAction>
<MenuAction>
<Link
href={`${PATHS.LINK.CREATE}?collectionId=${activeCollection?.id}`}
>
{t('link.create')}
</Link>
<kbd>L</kbd>
</MenuAction>
</MenuControls>
);
}

View File

@@ -1,69 +1,51 @@
import PATHS from '#constants/paths';
import styled from '@emotion/styled';
import CollectionList from '~/components/dashboard/side_nav/collection/collection_list';
import FavoriteList from '~/components/dashboard/side_nav/favorites/favorite_list';
import NavigationLinks from '~/components/dashboard/side_nav/navigation_links/navigation_links';
import UserCard from '~/components/dashboard/side_nav/user_card/user_card';
import { AiOutlineFolderAdd } from 'react-icons/ai';
import { BsGear } from 'react-icons/bs';
import { IoAdd } from 'react-icons/io5';
import { MdOutlineAdminPanelSettings } from 'react-icons/md';
import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list';
import { ItemLink } from '~/components/dashboard/side_nav/nav_item';
import UserCard from '~/components/dashboard/side_nav/user_card';
const SideMenu = styled.nav({
height: '100%',
width: '325px',
display: 'flex',
alignItems: 'center',
gap: '.35em',
flexDirection: 'column',
overflow: 'hidden',
});
const BlockWrapper = styled.section(({ theme }) => ({
height: 'auto',
width: '100%',
const AdminButton = styled(ItemLink)(({ theme }) => ({
color: theme.colors.lightRed,
}));
'& h4': {
userSelect: 'none',
textTransform: 'uppercase',
fontSize: '0.85em',
fontWeight: 500,
color: theme.colors.grey,
marginBottom: '5px',
},
const SettingsButton = styled(ItemLink)(({ theme }) => ({
color: theme.colors.grey,
}));
'& ul': {
flex: 1,
animation: 'fadein 0.3s both',
},
'& ul li': {
position: 'relative',
userSelect: 'none',
cursor: 'pointer',
height: 'fit-content',
width: '100%',
fontSize: '0.9em',
backgroundColor: theme.colors.white,
padding: '0.5em 1em',
borderBottom: `2px solid ${theme.colors.lightestGrey}`,
borderRadius: theme.border.radius,
transition: theme.transition.delay,
'&:not(:last-child)': {
marginBottom: '5px',
},
},
const AddButton = styled(ItemLink)(({ theme }) => ({
color: theme.colors.primary,
}));
const SideNavigation = () => (
<SideMenu>
<BlockWrapper>
<FavoriteList />
</BlockWrapper>
<BlockWrapper css={{ minHeight: '0', flex: '1' }}>
<CollectionList />
</BlockWrapper>
<BlockWrapper>
<NavigationLinks />
</BlockWrapper>
<BlockWrapper>
<div css={{ paddingInline: '10px' }}>
<UserCard />
</BlockWrapper>
<AdminButton href="/admin">
<MdOutlineAdminPanelSettings /> Administrator
</AdminButton>
<SettingsButton href="/settings">
<BsGear />
Settings
</SettingsButton>
<AddButton href={PATHS.LINK.CREATE}>
<IoAdd /> Create link
</AddButton>
<AddButton href={PATHS.COLLECTION.CREATE}>
<AiOutlineFolderAdd /> Create collection
</AddButton>
</div>
<FavoriteList />
</SideMenu>
);

View File

@@ -0,0 +1,16 @@
import RoundedImage from '~/components/common/rounded_image';
import { Item } from '~/components/dashboard/side_nav/nav_item';
import useUser from '~/hooks/use_user';
export default function UserCard() {
const { user, isAuthenticated } = useUser();
const altImage = `${user?.nickName}'s avatar`;
return (
isAuthenticated && (
<Item className="disable-hover">
<RoundedImage src={user.avatarUrl} width={24} alt={altImage} />
{user.nickName}
</Item>
)
);
}

View File

@@ -1,73 +0,0 @@
import PATHS from '#constants/paths';
import styled from '@emotion/styled';
import { Link } from '@inertiajs/react';
import { useTranslation } from 'react-i18next';
import { MdAdminPanelSettings } from 'react-icons/md';
import RoundedImage from '~/components/common/rounded_image';
import useUser from '~/hooks/use_user';
const UserCardWrapper = styled.div(({ theme }) => ({
userSelect: 'none',
height: 'fit-content',
width: '100%',
color: theme.colors.black,
backgroundColor: theme.colors.white,
border: `1px solid ${theme.colors.lightestGrey}`,
padding: '7px 12px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
'& button, & a': {
cursor: 'pointer',
color: theme.colors.blue,
display: 'flex',
transition: theme.transition.delay,
'&:hover': {
transform: 'scale(1.3)',
},
},
}));
const UserCardStyle = styled.div({
display: 'flex',
gap: '0.5em',
alignItems: 'center',
});
const UserControls = styled.div({
display: 'flex',
gap: '0.35em',
});
export default function UserCard() {
const { user } = useUser();
const { t } = useTranslation('common');
const avatarLabel = t('avatar', {
name: user.name,
});
return (
<UserCardWrapper>
<UserCardStyle>
<RoundedImage
src={user.avatarUrl}
width={28}
height={28}
alt={avatarLabel}
title={avatarLabel}
/>
{user.nickName}
</UserCardStyle>
<UserControls>
{user.is_admin && (
<Link href={PATHS.ADMIN} className="reset">
<MdAdminPanelSettings color="red" size={24} />
</Link>
)}
{/* <SettingsModal /> */}
</UserControls>
</UserCardWrapper>
);
}

View File

@@ -1,6 +1,6 @@
import styled from '@emotion/styled';
import { IoEarthOutline } from 'react-icons/io5';
import { Visibility } from '../../../../../app/enums/visibility';
import { Visibility } from '../../../app/enums/visibility';
const VisibilityStyle = styled.span(({ theme }) => ({
fontWeight: 300,

View File

@@ -5,8 +5,8 @@ const documentStyle = css({
'html, body, #app': {
height: '100svh',
width: '100svw',
fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
fontSize: '17px',
fontFamily: "'Poppins', sans-serif",
fontSize: '14px',
color: theme.colors.font,
backgroundColor: theme.colors.background,
display: 'flex',

View File

@@ -8,7 +8,7 @@ const white = '#fff';
const lightestGrey = '#dadce0';
const lightGrey = '#f0eef6';
const grey = '#aaa';
const grey = '#888888';
const darkGrey = '#4b5563';
const lightestBlue = '#d3e8fa';
@@ -17,7 +17,8 @@ const blue = '#3f88c5';
const darkBlue = '#005aa5';
const darkestBlue = '#1f2937';
const lightRed = '#ffbabab9';
const lightestRed = '#ffbabab9';
const lightRed = '#FF5A5A';
const red = '#d8000c';
const lightGreen = '#c1ffbab9';
@@ -48,6 +49,7 @@ export const theme: Theme = {
darkBlue,
darkestBlue,
lightestRed,
lightRed,
red,

View File

@@ -1,4 +1,11 @@
import { Serialize } from '@tuyau/utils/types';
import type UserModel from '../../app/models/user';
import type Collection from '#models/collection';
type User = Serialize<UserModel>;
type User = {
id: string;
email: string;
name: string;
nickName: string;
avatarUrl: string;
isAdmin: boolean;
collections: Collection[];
};

View File

@@ -24,6 +24,7 @@ declare module '@emotion/react' {
darkBlue: string;
darkestBlue: string;
lightestRed: string;
lightRed: string;
red: string;

View File

@@ -1,8 +1,17 @@
import type { User } from './app';
export type InertiaPage<T extends Record<string, unknown> = Record<string, unknown>> = T & {
auth: {
user?: User;
isAuthenticated: boolean;
};
type Auth =
| {
isAuthenticated: true;
user: User;
}
| {
isAuthenticated: false;
user: null;
};
export type InertiaPage<
T extends Record<string, unknown> = Record<string, unknown>,
> = T & {
auth: Auth;
};