Updates to personalization

This commit is contained in:
Sean Edmunds
2025-12-29 16:01:14 -06:00
parent c3cbf43647
commit c5f71b468c
13 changed files with 216 additions and 72 deletions

View File

@@ -32,6 +32,9 @@ function App() {
const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({ const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({
endpoint: 'api/themePlaylists', endpoint: 'api/themePlaylists',
}); });
const { data: personalData, isFetched: isPersonalDataFetched } = useQueryAPI({
endpoint: 'api/personalData',
});
const { const {
theme, theme,
setTheme, setTheme,
@@ -78,7 +81,7 @@ function App() {
); );
useEffect(() => { useEffect(() => {
if (allThemes && themeName) { if (allThemes && themeName && personalData) {
if (currentThemePlaylist === 'All') { if (currentThemePlaylist === 'All') {
if (themeName === 'random') { if (themeName === 'random') {
handleThemeChange(allThemes, false); handleThemeChange(allThemes, false);
@@ -124,6 +127,7 @@ function App() {
allThemes, allThemes,
currentThemePlaylist, currentThemePlaylist,
isThemePlaylistsFetched, isThemePlaylistsFetched,
isPersonalDataFetched,
]); ]);
return ( return (
<> <>

View File

@@ -40,7 +40,7 @@ export function SettingsHeader({
highlightList: string[]; highlightList: string[];
description: string; description: string;
}; };
searchValue: string | number; searchValue: string | number | undefined;
setSearchValue: (searchValue: string | number) => void; setSearchValue: (searchValue: string | number) => void;
searchPlaceholder: string | undefined; searchPlaceholder: string | undefined;
isLoading: boolean; isLoading: boolean;

View File

@@ -137,6 +137,10 @@ export function useMutationAPI() {
queryClient.refetchQueries({ queryKey: ['GET', 'api/themes'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/themes'] });
endpoint === 'api/providers' && endpoint === 'api/providers' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/providers/default'] }); queryClient.refetchQueries({ queryKey: ['GET', 'api/providers/default'] });
endpoint === 'api/personalData' &&
queryClient.refetchQueries({ queryKey: ['GET', 'api/personalData'] });
queryClient.refetchQueries({ queryKey: ['GET', 'api/wolEnabled'] });
queryClient.refetchQueries({ queryKey: ['GET', 'api/guacamoleEnabled'] });
}, },
}); });
} }

View File

@@ -220,6 +220,7 @@ export function Homepage() {
closeWeatherModal={closeWeatherModal} closeWeatherModal={closeWeatherModal}
openWeatherModal={openWeatherModal} openWeatherModal={openWeatherModal}
position='top' position='top'
personal={personal as PersonalData}
/> />
<Header <Header
greet={displayedGreeting} greet={displayedGreeting}
@@ -245,6 +246,7 @@ export function Homepage() {
closeWeatherModal={closeWeatherModal} closeWeatherModal={closeWeatherModal}
openWeatherModal={openWeatherModal} openWeatherModal={openWeatherModal}
position='center' position='center'
personal={personal as PersonalData}
/> />
</Box> </Box>
)} )}

View File

@@ -21,13 +21,13 @@ import type {
CurrentWeather, CurrentWeather,
ExtendedForecast, ExtendedForecast,
GuacamoleClient, GuacamoleClient,
PersonalData,
Provider, Provider,
SearchHistoryItem, SearchHistoryItem,
} from 'types'; } from 'types';
import RecommendedApplications from './components/RecommendedApplications'; import RecommendedApplications from './components/RecommendedApplications';
import RecommendedBookmarks from './components/RecommendedBookmarks'; import RecommendedBookmarks from './components/RecommendedBookmarks';
import SearchSelectItem from './components/SearchSelectItem'; import SearchSelectItem from './components/SearchSelectItem';
import { FaComputer } from 'react-icons/fa6';
export default function Search({ export default function Search({
current, current,
@@ -36,6 +36,7 @@ export default function Search({
closeWeatherModal, closeWeatherModal,
openWeatherModal, openWeatherModal,
position, position,
personal,
}: { }: {
current: CurrentWeather; current: CurrentWeather;
extendedForecast: ExtendedForecast[]; extendedForecast: ExtendedForecast[];
@@ -43,17 +44,19 @@ export default function Search({
closeWeatherModal: () => void; closeWeatherModal: () => void;
openWeatherModal: () => void; openWeatherModal: () => void;
position: string; position: string;
personal: PersonalData;
}) { }) {
const { ref, height } = useElementSize(); const { ref, height } = useElementSize();
const allIcons: any = { ...fa6, ...vsc, ...si }; const allIcons: any = { ...fa6, ...vsc, ...si };
const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({ const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({
endpoint: 'api/providers', endpoint: 'api/providers',
}); });
const [providers, setProviders] = useState<Provider[]>([]);
const [guacamoleClients, setGuacamoleClients] = useState<GuacamoleClient[]>([]);
const { data: applicationData, isFetching: isApplicationDataFetching } = useQueryAPI({ const { data: applicationData, isFetching: isApplicationDataFetching } = useQueryAPI({
endpoint: 'api/enabledApplications', endpoint: 'api/enabledApplications',
}); });
const { data: guacamoleHostData, isFetched: guacamoleHostDataIsFetched } = useQueryAPI({
endpoint: 'api/guacamoleHost',
});
const { data: guacamoleClientData, isFetched: guacamoleClientDataIsFetched } = const { data: guacamoleClientData, isFetched: guacamoleClientDataIsFetched } =
useQueryAPI({ useQueryAPI({
endpoint: 'api/guacamoleClients', endpoint: 'api/guacamoleClients',
@@ -65,6 +68,9 @@ export default function Search({
useQueryAPI({ useQueryAPI({
endpoint: 'api/providers/default', endpoint: 'api/providers/default',
}); });
const [providers, setProviders] = useState<Provider[]>([]);
const [guacamoleHost, setGuacamoleHost] = useState<string | undefined>('');
const [guacamoleClients, setGuacamoleClients] = useState<GuacamoleClient[]>([]);
const [applications, setApplications] = useState<Application[]>([]); const [applications, setApplications] = useState<Application[]>([]);
const [filteredApplications, setFilteredApplications] = useState<Application[]>([]); const [filteredApplications, setFilteredApplications] = useState<Application[]>([]);
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
@@ -130,7 +136,7 @@ export default function Search({
}; };
const handleGuacamoleClientLaunchItem = ({ option }: { option: any }) => { const handleGuacamoleClientLaunchItem = ({ option }: { option: any }) => {
const validUrl = `https://sean.bet/guacamole/#/client/${option.clientIdentifier}`; const validUrl = `${guacamoleHost}/#/client/${option.clientIdentifier}`;
open(validUrl, '_self'); open(validUrl, '_self');
}; };
@@ -175,7 +181,7 @@ export default function Search({
(client: GuacamoleClient) => (client: GuacamoleClient) =>
client.clientName.toLowerCase() === item.target.value.toLowerCase().substr(1), client.clientName.toLowerCase() === item.target.value.toLowerCase().substr(1),
); );
open(`https://sean.bet/guacamole/#/client/${client?.clientIdentifier}`, '_self'); open(`${guacamoleHost}/#/client/${client?.clientIdentifier}`, '_self');
} else if (urlPattern.test(item.target.value)) { } else if (urlPattern.test(item.target.value)) {
if (/^(https?:\/\/).*/i.test(item.target.value)) if (/^(https?:\/\/).*/i.test(item.target.value))
globalThis.location = item.target.value; globalThis.location = item.target.value;
@@ -301,7 +307,7 @@ export default function Search({
} else { } else {
setProviderLabel('Invalid function'); setProviderLabel('Invalid function');
} }
} else if (item.target.value.startsWith('>')) { } else if (item.target.value.startsWith('>') && personal.enableGuacamole === 1) {
if ( if (
guacamoleClients?.find( guacamoleClients?.find(
(client: GuacamoleClient) => (client: GuacamoleClient) =>
@@ -335,7 +341,7 @@ export default function Search({
}; };
}); });
} }
if (splittedSearch[0].startsWith('>')) { if (splittedSearch[0].startsWith('>') && personal.enableGuacamole === 1) {
return guacamoleClientData return guacamoleClientData
.filter((guacamoleClient: GuacamoleClient) => .filter((guacamoleClient: GuacamoleClient) =>
guacamoleClient.clientName guacamoleClient.clientName
@@ -380,15 +386,16 @@ export default function Search({
}; };
const renderItem: any = (item: any) => { const renderItem: any = (item: any) => {
const Icon = item.option.icon ? const Icon = item.option.icon
(allIcons[ ? allIcons[
item.option.icon item.option.icon
? item.option.icon ? item.option.icon
: item.option.prefix === '/g' : item.option.prefix === '/g'
? 'FaGoogle' ? 'FaGoogle'
: (providers.find((p: Provider) => p.prefix === item.option.prefix)?.icon ?? : (providers.find((p: Provider) => p.prefix === item.option.prefix)?.icon ??
'') '')
]) : (allIcons['FaComputer']); ]
: allIcons.FaComputer;
return (searchValue as string).startsWith('!') ? ( return (searchValue as string).startsWith('!') ? (
<Group <Group
pos={'relative'} pos={'relative'}
@@ -419,7 +426,7 @@ export default function Search({
{item.option.name} {item.option.name}
</Text> </Text>
</Group> </Group>
) : (searchValue as string).startsWith('>') ? ( ) : (searchValue as string).startsWith('>') && personal.enableGuacamole === 1 ? (
<Group <Group
pos={'relative'} pos={'relative'}
m={0} m={0}
@@ -486,6 +493,12 @@ export default function Search({
} }
}, [providerData, providerDataIsFetched]); }, [providerData, providerDataIsFetched]);
useEffect(() => {
if (guacamoleHostData && guacamoleHostDataIsFetched) {
setGuacamoleHost(guacamoleHostData[0].guacamoleHost);
}
}, [guacamoleClientData, guacamoleClientDataIsFetched]);
useEffect(() => { useEffect(() => {
if (guacamoleClientData && guacamoleClientDataIsFetched) { if (guacamoleClientData && guacamoleClientDataIsFetched) {
setGuacamoleClients(guacamoleClientData); setGuacamoleClients(guacamoleClientData);

View File

@@ -16,8 +16,11 @@ function SettingsButton() {
const { data: wolClientData, isFetched: wolClientDataIsFetched } = useQueryAPI({ const { data: wolClientData, isFetched: wolClientDataIsFetched } = useQueryAPI({
endpoint: 'api/wolClients', endpoint: 'api/wolClients',
}); });
const { data: wolEnabledData, isFetched: wolEnabledDataIsFetched } = useQueryAPI({
endpoint: 'api/wolEnabled',
});
const [wolClients, setWolClients] = useState<WolClient[]>([]); const [wolClients, setWolClients] = useState<WolClient[]>([]);
const [enabled, setEnabled] = useState<number>(0);
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [categories, setCategories] = useState([{ value: 'All', label: 'All' }]); const [categories, setCategories] = useState([{ value: 'All', label: 'All' }]);
const [selectedClient, setSelectedClient] = useState<WolClient>(wolClientData?.[0]); const [selectedClient, setSelectedClient] = useState<WolClient>(wolClientData?.[0]);
@@ -67,6 +70,11 @@ function SettingsButton() {
} }
}, [wolClientData, wolClientDataIsFetched]); }, [wolClientData, wolClientDataIsFetched]);
useEffect(() => {
if (wolEnabledData && wolEnabledDataIsFetched) {
setEnabled(wolEnabledData[0].enableWol);
}
}, [wolEnabledData, wolEnabledDataIsFetched]);
return ( return (
<Menu <Menu
classNames={menuClasses} classNames={menuClasses}
@@ -100,21 +108,23 @@ function SettingsButton() {
width: isMobile ? '100%' : undefined, width: isMobile ? '100%' : undefined,
}} }}
> >
<Box {enabled === 1 && (
pos={'relative'} <Box
style={{ filter: wolClients.length === 0 ? 'grayscale(1)' : undefined }} pos={'relative'}
> style={{ filter: wolClients.length === 0 ? 'grayscale(1)' : undefined }}
{wolClients.length === 0 && ( >
<Overlay opacity={0.75} style={{ borderRadius: '10px' }} /> {wolClients.length === 0 && (
)} <Overlay opacity={0.75} style={{ borderRadius: '10px' }} />
<WakeOnLan )}
wolClients={wolClients} <WakeOnLan
selectedClient={selectedClient} wolClients={wolClients}
setSelectedClient={setSelectedClient} selectedClient={selectedClient}
handleWakeRequest={handleWakeRequest} setSelectedClient={setSelectedClient}
/> handleWakeRequest={handleWakeRequest}
</Box> />
<ThemingCard /> </Box>
)}
<ThemingCard wolEnabled={enabled} />
</Stack> </Stack>
<Stack> <Stack>
<SettingsCard <SettingsCard

View File

@@ -41,6 +41,7 @@ function SettingsCard({
className={stackClasses.menuStack} className={stackClasses.menuStack}
classNames={stackClasses} classNames={stackClasses}
pos={'relative'} pos={'relative'}
h={150}
> >
<Text <Text
style={{ style={{

View File

@@ -22,7 +22,7 @@ import {
} from 'styles'; } from 'styles';
import type { Theme, ThemePlaylist } from 'types'; import type { Theme, ThemePlaylist } from 'types';
function ThemingCard() { function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
const { data: themePlaylistData } = useQueryAPI({ const { data: themePlaylistData } = useQueryAPI({
endpoint: 'api/themePlaylists', endpoint: 'api/themePlaylists',
}); });
@@ -69,19 +69,35 @@ function ThemingCard() {
return ( return (
<Stack <Stack
gap={0} gap={0}
justify='space-between' justify={wolEnabled === 0 ? undefined : 'space-between'}
className={stackClasses.menuStack} className={stackClasses.menuStack}
classNames={stackClasses} classNames={stackClasses}
pos={'relative'} pos={'relative'}
style={{ aspectRatio: isMobile ? undefined : '1/1' }} style={{
aspectRatio: isMobile || wolEnabled === 0 ? undefined : '1/1',
height: wolEnabled === 0 ? '316px' : '150px',
width: '150px',
}}
> >
<Menu.Label pb={'2rem'}>Themes</Menu.Label> <Menu.Label pb={'2rem'}>Themes</Menu.Label>
<Group mt={'-1.5rem'} mb={'-.25rem'} w={'95%'} h={'100%'} gap={0}> <Group
<Stack gap={0}> mt={'-1.5rem'}
mb={'-.25rem'}
w={'95%'}
h={wolEnabled === 0 ? undefined : '100%'}
gap={0}
>
<Stack gap={0} h={'fit-content'} w={'73%'}>
<Text className={textClasses.currentTheme} classNames={textClasses}> <Text className={textClasses.currentTheme} classNames={textClasses}>
Current theme: Current theme:
</Text> </Text>
<Text className={textClasses.themeName} classNames={textClasses}> <Text
w={'100%'}
ml={4}
mt={wolEnabled === 0 ? '1rem' : undefined}
className={textClasses.themeName}
classNames={textClasses}
>
{text} {text}
</Text> </Text>
</Stack> </Stack>

View File

@@ -22,7 +22,11 @@ function WakeOnLan({
pos={'relative'} pos={'relative'}
className={stackClasses.menuStack} className={stackClasses.menuStack}
classNames={stackClasses} classNames={stackClasses}
style={{ aspectRatio: isMobile ? undefined : '1/1' }} style={{
aspectRatio: isMobile ? undefined : '1/1',
height: '150px',
alignItems: 'center',
}}
> >
<Menu.Label>Wake on LAN</Menu.Label> <Menu.Label>Wake on LAN</Menu.Label>
<Button <Button

View File

@@ -1,5 +1,6 @@
import { ActionIcon, Divider, Group, ScrollArea, Stack, Tabs, Text } from '@mantine/core'; import { ActionIcon, Divider, Group, ScrollArea, Stack, Tabs, Text } from '@mantine/core';
import { useCommonHooks } from 'hooks'; import { useCommonHooks, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FaPaintBrush, FaRegListAlt } from 'react-icons/fa'; import { FaPaintBrush, FaRegListAlt } from 'react-icons/fa';
import { import {
FiArrowLeftCircle, FiArrowLeftCircle,
@@ -12,6 +13,7 @@ import {
} from 'react-icons/fi'; } from 'react-icons/fi';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { actionIconClasses, scrollAreaClasses, tabClasses, textClasses } from 'styles'; import { actionIconClasses, scrollAreaClasses, tabClasses, textClasses } from 'styles';
import type { PersonalData } from 'types';
import Applications from './components/Applications/Applications'; import Applications from './components/Applications/Applications';
import Bookmarks from './components/Bookmarks/Bookmarks'; import Bookmarks from './components/Bookmarks/Bookmarks';
import GuacamoleClients from './components/GuacamoleClients/GuacamoleClients'; import GuacamoleClients from './components/GuacamoleClients/GuacamoleClients';
@@ -24,9 +26,20 @@ import { Themes } from './components/Theming/Themes';
import WolClients from './components/WoL/WolClients'; import WolClients from './components/WoL/WolClients';
export function Settings() { export function Settings() {
const endpoint = 'api/personalData';
const { data: personalData, isFetched: personalDataIsFetched } = useQueryAPI({
endpoint: endpoint,
});
const [personal, setPersonal] = useState<PersonalData>();
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
const navigate = useNavigate(); const navigate = useNavigate();
const { tabValue } = useParams(); const { tabValue } = useParams();
useEffect(() => {
if (personalData && personalDataIsFetched) {
setPersonal(personalData[0]);
}
}, [personalData, personalDataIsFetched]);
return ( return (
<Stack h={'100%'} gap={0}> <Stack h={'100%'} gap={0}>
<Group gap={0} style={{ position: 'relative', paddingBottom: '1rem' }}> <Group gap={0} style={{ position: 'relative', paddingBottom: '1rem' }}>
@@ -142,7 +155,7 @@ export function Settings() {
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='history'> <Tabs.Tab px={isMobile ? '.9rem' : undefined} value='history'>
{isMobile ? <FiSearch /> : 'Search History'} {isMobile ? <FiSearch /> : 'Search History'}
</Tabs.Tab> </Tabs.Tab>
{!isMobile && ( {!isMobile && (personal?.enableWol || personal?.enableGuacamole) ? (
<Group w={'100%'} gap={0} justify='center'> <Group w={'100%'} gap={0} justify='center'>
<Divider <Divider
label='Extras' label='Extras'
@@ -159,13 +172,17 @@ export function Settings() {
}} }}
/> />
</Group> </Group>
)} ) : null}
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='wolClients'> {personal?.enableWol ? (
{isMobile ? <FiMonitor /> : 'WoL Clients'} <Tabs.Tab px={isMobile ? '.9rem' : undefined} value='wolClients'>
</Tabs.Tab> {isMobile ? <FiMonitor /> : 'WoL Clients'}
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='guacamoleClients'> </Tabs.Tab>
{isMobile ? <FiMonitor /> : 'Guacamole Clients'} ) : null}
</Tabs.Tab> {personal?.enableGuacamole ? (
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='guacamoleClients'>
{isMobile ? <FiMonitor /> : 'Guacamole Clients'}
</Tabs.Tab>
) : null}
</Tabs.List> </Tabs.List>
<Tabs.Panel value='personalization'> <Tabs.Panel value='personalization'>
<ScrollArea <ScrollArea
@@ -231,22 +248,26 @@ export function Settings() {
<SearchHistory /> <SearchHistory />
</ScrollArea> </ScrollArea>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value='wolClients'> {personal?.enableWol ? (
<ScrollArea <Tabs.Panel value='wolClients'>
className={scrollAreaClasses.default} <ScrollArea
classNames={scrollAreaClasses} className={scrollAreaClasses.default}
> classNames={scrollAreaClasses}
<WolClients /> >
</ScrollArea> <WolClients />
</Tabs.Panel> </ScrollArea>
<Tabs.Panel value='guacamoleClients'> </Tabs.Panel>
<ScrollArea ) : null}
className={scrollAreaClasses.default} {personal?.enableGuacamole ? (
classNames={scrollAreaClasses} <Tabs.Panel value='guacamoleClients'>
> <ScrollArea
<GuacamoleClients /> className={scrollAreaClasses.default}
</ScrollArea> classNames={scrollAreaClasses}
</Tabs.Panel> >
<GuacamoleClients />
</ScrollArea>
</Tabs.Panel>
) : null}
</Tabs> </Tabs>
</Stack> </Stack>
); );

View File

@@ -1,9 +1,18 @@
import { Card, Grid, GridCol, LoadingOverlay, Stack, TextInput } from '@mantine/core'; import {
Card,
Grid,
GridCol,
LoadingOverlay,
Stack,
Switch,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { SettingsHeader } from 'components'; import { SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { cardClasses, textInputClasses } from 'styles'; import { cardClasses, switchClasses, textClasses, textInputClasses } from 'styles';
import type { PersonalData } from 'types'; import type { PersonalData } from 'types';
const PersonalizationSettings = () => { const PersonalizationSettings = () => {
@@ -40,7 +49,6 @@ const PersonalizationSettings = () => {
setPersonal(update); setPersonal(update);
} }
}, [update]); }, [update]);
return ( return (
<Stack> <Stack>
<SettingsHeader <SettingsHeader
@@ -175,6 +183,66 @@ const PersonalizationSettings = () => {
</Card> </Card>
</GridCol> </GridCol>
</Grid> </Grid>
<Card
w={'75%'}
className={cardClasses.settingsItem}
classNames={cardClasses}
style={{ overflow: 'visible' }}
>
<Stack w={'100%'} px={16}>
<Text top={-12} className={textClasses.currentTheme} classNames={textClasses}>
Extras
</Text>
<Switch
label={'Enable Wake on LAN'}
className={switchClasses.default}
classNames={switchClasses}
checked={personal?.enableWol === 1}
onChange={(event) => {
const updatedPersonal = {
...personal,
enableWol: event.target.checked ? 1 : 0,
};
setUpdate(updatedPersonal);
}}
/>
<Switch
label={'Enable Guacamole Clients'}
className={switchClasses.default}
classNames={switchClasses}
checked={personal?.enableGuacamole === 1}
onChange={(event) => {
const updatedPersonal = {
...personal,
enableGuacamole: event.target.checked ? 1 : 0,
};
setUpdate(updatedPersonal);
}}
/>
{personal?.enableGuacamole === 1 && (
<TextInput
label={'Guacamole Host'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.guacamoleHost}
onChange={(event) => {
const updatedPersonal = {
...personal,
guacamoleHost: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
)}
</Stack>
</Card>
</Stack> </Stack>
</Stack> </Stack>
); );

View File

@@ -13,10 +13,8 @@
} }
.menuStack { .menuStack {
height: 150px;
background-color: var(--mantine-color-search-0); background-color: var(--mantine-color-search-0);
border-radius: 10px; border-radius: 10px;
align-items: center !important;
} }
.fadeAway { .fadeAway {

View File

@@ -3,4 +3,7 @@ export interface PersonalData {
lastName: string; lastName: string;
email: string; email: string;
physicalAddress: string; physicalAddress: string;
enableWol: number;
enableGuacamole: number;
guacamoleHost?: string;
} }