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

View File

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

View File

@@ -137,6 +137,10 @@ export function useMutationAPI() {
queryClient.refetchQueries({ queryKey: ['GET', 'api/themes'] });
endpoint === 'api/providers' &&
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}
openWeatherModal={openWeatherModal}
position='top'
personal={personal as PersonalData}
/>
<Header
greet={displayedGreeting}
@@ -245,6 +246,7 @@ export function Homepage() {
closeWeatherModal={closeWeatherModal}
openWeatherModal={openWeatherModal}
position='center'
personal={personal as PersonalData}
/>
</Box>
)}

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ import {
} from 'styles';
import type { Theme, ThemePlaylist } from 'types';
function ThemingCard() {
function ThemingCard({ wolEnabled }: { wolEnabled: number }) {
const { data: themePlaylistData } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
@@ -69,19 +69,35 @@ function ThemingCard() {
return (
<Stack
gap={0}
justify='space-between'
justify={wolEnabled === 0 ? undefined : 'space-between'}
className={stackClasses.menuStack}
classNames={stackClasses}
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>
<Group mt={'-1.5rem'} mb={'-.25rem'} w={'95%'} h={'100%'} gap={0}>
<Stack gap={0}>
<Group
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}>
Current theme:
</Text>
<Text className={textClasses.themeName} classNames={textClasses}>
<Text
w={'100%'}
ml={4}
mt={wolEnabled === 0 ? '1rem' : undefined}
className={textClasses.themeName}
classNames={textClasses}
>
{text}
</Text>
</Stack>

View File

@@ -22,7 +22,11 @@ function WakeOnLan({
pos={'relative'}
className={stackClasses.menuStack}
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>
<Button

View File

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

View File

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

View File

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