Customizable fonts

This commit is contained in:
Sean Edmunds
2026-01-28 14:27:27 -06:00
parent f92ee29dfa
commit 983fcbe2ae
7 changed files with 435 additions and 358 deletions

View File

@@ -19,6 +19,7 @@ import { Loading, MainLayout } from './components';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import 'react-fontpicker-ts/dist/index.css';
import '@gfazioli/mantine-text-animate/styles.css';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
@@ -54,7 +55,7 @@ function App() {
search: colorsTuple(theme.searchColor),
},
white: theme?.backgroundColor,
fontFamily: '-apple-system,Jost,BlinkMacSystemFont,Helvetica Neue,Roboto,sans-serif',
fontFamily: `${personalData?.[0]?.defaultFont},Helvetica Neue,Roboto,system-ui,sans-serif`,
autoContrast: true,
});
const mantineTheme = mergeMantineTheme(DEFAULT_THEME, overrides);
@@ -81,7 +82,12 @@ function App() {
);
useEffect(() => {
if (allThemes && themeName && personalData && isThemePlaylistsFetched) {
if (
allThemes &&
themeName &&
Object.keys(personalData).length > 0 &&
isThemePlaylistsFetched
) {
if (currentThemePlaylist === 'All') {
if (themeName === 'random') {
handleThemeChange(allThemes, false);

View File

@@ -12,6 +12,7 @@ import { useDebouncedValue } from '@mantine/hooks';
import { SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import FontPicker from 'react-fontpicker-ts';
import { cardClasses, switchClasses, textClasses, textInputClasses } from 'styles';
import type { PersonalData } from 'types';
@@ -20,25 +21,44 @@ const PersonalizationSettings = () => {
const { data: personalData, isFetching: personalDataIsFetching } = useQueryAPI({
endpoint: endpoint,
});
const [personal, setPersonal] = useState<PersonalData>(personalData?.[0] || {});
const personal: PersonalData = personalData?.[0] || {};
const [wolEnabled, setWolEnabled] = useState<boolean>(personal?.enableWol === 1);
const [guacamoleEnabled, setGuacamoleEnabled] = useState<boolean>(
personal?.enableGuacamole === 1,
);
const [defaultFont, setDefaultFont] = useState<string>(personal?.defaultFont);
const [update, setUpdate] = useState<any>({});
const [debounced] = useDebouncedValue(update, 1000);
const { mutateAsync } = useMutationAPI();
const { isMobile, theme } = useCommonHooks();
useEffect(() => {
if (Object.keys(debounced).length) {
setPersonal(update);
mutateAsync({ method: 'PATCH', endpoint: endpoint, body: debounced });
setUpdate({});
}
}, [debounced]);
useEffect(() => {
if (update !== personal && Object.keys(update).length) {
setPersonal(update);
if (Object.keys(personal).length > 0) {
setWolEnabled(personal.enableWol === 1);
setGuacamoleEnabled(personal.enableGuacamole === 1);
setDefaultFont(personal.defaultFont);
}
}, [update]);
}, [personal]);
useEffect(() => {
if (
Object.keys(personal).length > 0 &&
defaultFont &&
defaultFont !== personal.defaultFont
) {
setUpdate({
...personal,
defaultFont: defaultFont,
});
}
}, [defaultFont]);
return (
<Stack>
<SettingsHeader
@@ -76,163 +96,229 @@ const PersonalizationSettings = () => {
}}
/>
<Grid w={'75%'}>
<GridCol span={isMobile ? 12 : 4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}>
<TextInput
label={'First Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.firstName}
onChange={(event) => {
const updatedPersonal = {
...personal,
firstName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={isMobile ? 12 : 4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}>
<TextInput
label={'Last Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.lastName}
onChange={(event) => {
const updatedPersonal = {
...personal,
lastName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={isMobile ? 12 : 4}>
<Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}>
<TextInput
label={'Email'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.email}
onChange={(event) => {
const updatedPersonal = {
...personal,
email: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={12}>
<Card className={cardClasses.settingsItem} classNames={cardClasses} pt={0}>
<TextInput
label={'Physical Address'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.physicalAddress}
onChange={(event) => {
const updatedPersonal = {
...personal,
physicalAddress: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
{Object.keys(personal).length > 0 && (
<>
<GridCol span={isMobile ? 12 : 4}>
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
pt={0}
>
<TextInput
label={'First Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.firstName}
onChange={(event) => {
const updatedPersonal = {
...personal,
firstName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={isMobile ? 12 : 4}>
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
pt={0}
>
<TextInput
label={'Last Name'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.lastName}
onChange={(event) => {
const updatedPersonal = {
...personal,
lastName: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={isMobile ? 12 : 4}>
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
pt={0}
>
<TextInput
label={'Email'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.email}
onChange={(event) => {
const updatedPersonal = {
...personal,
email: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={12}>
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
pt={0}
>
<TextInput
label={'Nickname'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.nickname || ''}
onChange={(event) => {
const updatedPersonal = {
...personal,
nickname: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={12}>
<Card
className={cardClasses.settingsItem}
classNames={cardClasses}
pt={0}
>
<TextInput
label={'Physical Address'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
defaultValue={personal?.physicalAddress}
onChange={(event) => {
const updatedPersonal = {
...personal,
physicalAddress: event.currentTarget.value,
};
setUpdate(updatedPersonal);
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
/>
</Card>
</GridCol>
<GridCol span={12}>
<Card
w={'100%'}
className={cardClasses.settingsItem}
classNames={cardClasses}
style={{ overflow: 'visible' }}
>
<Stack w={'100%'} px={16}>
<Text
top={-12}
className={textClasses.currentTheme}
classNames={textClasses}
>
Extras
</Text>
<Text>Font Select</Text>
{defaultFont && (
<FontPicker
autoLoad
defaultValue={defaultFont || 'Jost'}
value={(font: string) => setDefaultFont(font)}
/>
)}
<Switch
label={'Enable Wake on LAN'}
className={switchClasses.default}
classNames={switchClasses}
checked={wolEnabled}
onChange={(event) => {
setWolEnabled(!wolEnabled);
const updatedPersonal = {
...personal,
enableWol: event.target.checked ? 1 : 0,
};
setUpdate(updatedPersonal);
}}
/>
<Switch
label={'Enable Guacamole Clients'}
className={switchClasses.default}
classNames={switchClasses}
checked={guacamoleEnabled}
onChange={(event) => {
setGuacamoleEnabled(!guacamoleEnabled);
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>
</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

@@ -10,8 +10,6 @@
.dateTime {
&.root {
display: flex;
font-family: -apple-system, Jost, BlinkMacSystemFont, Helvetica Neue, Roboto,
sans-serif !important;
font-size: 1.75rem;
margin: 0;
padding: 0;
@@ -78,7 +76,6 @@
font-size: .6875rem;
font-style: normal;
color: var(--mantine-color-primary-0);
font-family: "Jost", sans-serif;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -6,4 +6,6 @@ export interface PersonalData {
enableWol: number;
enableGuacamole: number;
guacamoleHost?: string;
nickname?: string;
defaultFont: string;
}