MainLayout for theme persistence
This commit is contained in:
44
src/App.tsx
44
src/App.tsx
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Box,
|
||||
DEFAULT_THEME,
|
||||
MantineProvider,
|
||||
createTheme,
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
} from 'react-router-dom';
|
||||
import { ThemeProvider } from './components';
|
||||
import { MainLayout, ThemeProvider } from './components';
|
||||
import '@mantine/core/styles.css'; //import Mantine V7 styles needed by MRT
|
||||
import '@mantine/dates/styles.css'; //if using mantine date picker features
|
||||
import 'mantine-react-table/styles.css'; //import MRT styles
|
||||
@@ -57,15 +56,20 @@ function App() {
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route>
|
||||
<Route path='/' element={<Navigate to={`/${import.meta.env.VITE_BASE_URL}`} />} />
|
||||
<Route
|
||||
path={`/${import.meta.env.VITE_BASE_URL}`}
|
||||
element={<Homepage theme={theme} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/${import.meta.env.VITE_BASE_URL}/settings`}
|
||||
element={<Settings theme={theme} />}
|
||||
/>
|
||||
<Route path='' element={<MainLayout theme={theme} />}>
|
||||
<Route
|
||||
path='/'
|
||||
element={<Navigate to={`/${import.meta.env.VITE_BASE_URL}`} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/${import.meta.env.VITE_BASE_URL}`}
|
||||
element={<Homepage theme={theme} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/${import.meta.env.VITE_BASE_URL}/settings`}
|
||||
element={<Settings theme={theme} setTheme={setTheme} allThemes={data} />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
@@ -82,22 +86,8 @@ function App() {
|
||||
<MantineProvider theme={mantineTheme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModalsProvider>
|
||||
<>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
backgroundImage: `linear-gradient(${theme.backgroundColor}33, ${theme.backgroundColor}33), url(/backgrounds/${theme.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
<RouterProvider router={router} />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</>
|
||||
<RouterProvider router={router} />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</ModalsProvider>
|
||||
</ThemeProvider>
|
||||
</MantineProvider>
|
||||
|
||||
50
src/Root.tsx
Normal file
50
src/Root.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
PersistQueryClientProvider,
|
||||
type PersistedClient,
|
||||
type Persister,
|
||||
} from '@tanstack/react-query-persist-client';
|
||||
import { del, get, set } from 'idb-keyval';
|
||||
import React, { type ComponentType } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
const container = document.getElementById('root') as HTMLElement;
|
||||
const root = createRoot(container);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
staleTime: 1000 * 60 * 1, // 1 min
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function createIDBPersister(idbValidKey: IDBValidKey = 'reactQuery') {
|
||||
return {
|
||||
persistClient: async (client: PersistedClient) => {
|
||||
set(idbValidKey, client);
|
||||
},
|
||||
restoreClient: async () => {
|
||||
return await get<PersistedClient>(idbValidKey);
|
||||
},
|
||||
removeClient: async () => {
|
||||
await del(idbValidKey);
|
||||
},
|
||||
} as Persister;
|
||||
}
|
||||
|
||||
function render(App: ComponentType) {
|
||||
root.render(
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{ persister: createIDBPersister() }}
|
||||
>
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
</PersistQueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
export default render;
|
||||
21
src/components/MainLayout.tsx
Normal file
21
src/components/MainLayout.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Box } from '@mantine/core';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
export function MainLayout({ theme }: any) {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
backgroundImage: `linear-gradient(${theme.backgroundColor}33, ${theme.backgroundColor}33), url(/backgrounds/${theme.backgroundImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
zIndex: -1,
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export function WeatherModal({
|
||||
hourly,
|
||||
closeWeatherModal,
|
||||
weatherModalOpened,
|
||||
setValue,
|
||||
setProviderLabel,
|
||||
setProviderURL,
|
||||
isMobile,
|
||||
@@ -50,7 +49,6 @@ export function WeatherModal({
|
||||
opened={weatherModalOpened}
|
||||
onClose={() => {
|
||||
closeWeatherModal();
|
||||
setValue('');
|
||||
setProviderLabel('Google Search');
|
||||
setProviderURL('https://www.google.com/search?q=');
|
||||
}}
|
||||
@@ -119,7 +117,9 @@ export function WeatherModal({
|
||||
>
|
||||
{hourlyForecast.length > 0 &&
|
||||
hourlyForecast.map((h: any) => {
|
||||
return <HourlyCard key={h.time} weather={h} isMobile={isMobile} />;
|
||||
return (
|
||||
<HourlyCard key={h.number} weather={h} isMobile={isMobile} />
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
@@ -2,6 +2,7 @@ export { HelpModal } from './HelpModal/HelpModal';
|
||||
export { WeatherModal } from './WeatherModal/WeatherModal';
|
||||
export { DataTable } from './DataTable/DataTable';
|
||||
export { ThemeProvider } from './ThemeProvider';
|
||||
export { MainLayout } from './MainLayout';
|
||||
export { ApplicationColumns } from './DataTable/columns/ApplicationColumns';
|
||||
export { BookmarkColumns } from './DataTable/columns/BookmarkColumns';
|
||||
export { CategoryColumns } from './DataTable/columns/CategoryColumns';
|
||||
|
||||
29
src/main.tsx
29
src/main.tsx
@@ -1,21 +1,10 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import App from './App';
|
||||
import render from './Root';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
staleTime: 1000 * 60 * 1, // 1 min
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
Promise.all([render, App])
|
||||
.then(([render, App]) => {
|
||||
render(App);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error loading the app:', error);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Homepage({ theme }: any) {
|
||||
if (data) {
|
||||
setDisplayedGreeting(greet(data));
|
||||
}
|
||||
}, [isFetching]);
|
||||
}, [data, isFetching]);
|
||||
|
||||
useEffect(() => {
|
||||
if (weather && basicWeather) {
|
||||
|
||||
@@ -8,10 +8,10 @@ function Applications({ contentType, isMobile }: any) {
|
||||
const { data, isFetching } = useQueryAPI({ endpoint: 'api/apps' });
|
||||
const [applications, setApplications] = useState([]);
|
||||
useEffect(() => {
|
||||
if (!isFetching) {
|
||||
if (data && !isFetching) {
|
||||
setApplications(data);
|
||||
}
|
||||
}, [isFetching]);
|
||||
}, [data, isFetching]);
|
||||
return (
|
||||
<>
|
||||
{applications.length > 0 && contentType.length > 0 && (
|
||||
|
||||
@@ -16,7 +16,6 @@ function Header({
|
||||
weatherModalOpened,
|
||||
closeWeatherModal,
|
||||
openWeatherModal,
|
||||
setValue,
|
||||
setProviderLabel,
|
||||
setProviderURL,
|
||||
}: any) {
|
||||
@@ -155,7 +154,6 @@ function Header({
|
||||
hourly={hourly}
|
||||
closeWeatherModal={closeWeatherModal}
|
||||
weatherModalOpened={weatherModalOpened}
|
||||
setValue={setValue}
|
||||
setProviderLabel={setProviderLabel}
|
||||
setProviderURL={setProviderURL}
|
||||
isMobile={isMobile}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Menu, MultiSelect, NavLink } from '@mantine/core';
|
||||
import { useQueryAPI } from 'hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FiMenu, FiMonitor } from 'react-icons/fi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { buttonClasses, menuClasses, navLinkClasses, selectClasses } from 'styles';
|
||||
|
||||
function SettingsButton({
|
||||
@@ -12,7 +13,7 @@ function SettingsButton({
|
||||
}: any) {
|
||||
const { data, isFetched } = useQueryAPI({ endpoint: 'api/appCategories' });
|
||||
const [categories, setCategories] = useState([{ value: 'All', label: 'All' }]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const list: [any] =
|
||||
@@ -42,9 +43,9 @@ function SettingsButton({
|
||||
<NavLink
|
||||
label='Settings'
|
||||
component='a'
|
||||
href={`/${import.meta.env.VITE_BASE_URL}/settings`}
|
||||
className={navLinkClasses.defaultNavLink}
|
||||
classNames={navLinkClasses}
|
||||
onClick={() => navigate(`/${import.meta.env.VITE_BASE_URL}/settings`)}
|
||||
/>
|
||||
<NavLink
|
||||
label='Quick Actions'
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
FiSearch,
|
||||
FiUser,
|
||||
} from 'react-icons/fi';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { actionIconClasses, tabClasses, textClasses } from 'styles';
|
||||
import Applications from './components/Applications';
|
||||
import Bookmarks from './components/Bookmarks';
|
||||
@@ -18,8 +19,9 @@ import Personalization from './components/Personalization';
|
||||
import SearchProviders from './components/SearchProviders';
|
||||
import Themes from './components/Themes';
|
||||
|
||||
export function Settings({ theme }: any) {
|
||||
export function Settings({ theme, setTheme, allThemes }: any) {
|
||||
const isMobile = window.matchMedia('(max-width: 600px)').matches;
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Stack px={'2rem'}>
|
||||
<Group gap={0}>
|
||||
@@ -27,7 +29,7 @@ export function Settings({ theme }: any) {
|
||||
className={actionIconClasses.backButton}
|
||||
classNames={actionIconClasses}
|
||||
component='a'
|
||||
href={`/${import.meta.env.VITE_BASE_URL}`}
|
||||
onClick={() => navigate(`/${import.meta.env.VITE_BASE_URL}`)}
|
||||
>
|
||||
<FiArrowLeftCircle />
|
||||
</ActionIcon>
|
||||
@@ -74,7 +76,7 @@ export function Settings({ theme }: any) {
|
||||
<Categories />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value='themes'>
|
||||
<Themes theme={theme} />
|
||||
<Themes theme={theme} setTheme={setTheme} allThemes={allThemes} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value='providers'>
|
||||
<SearchProviders />
|
||||
|
||||
@@ -7,12 +7,15 @@ function ThemeCard({
|
||||
handleOpen,
|
||||
handleDeleteTheme,
|
||||
theme,
|
||||
allThemes,
|
||||
setTheme,
|
||||
cardTheme,
|
||||
currentTheme,
|
||||
setValue,
|
||||
createTheme,
|
||||
}: any) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [shift, setShift] = useState<number>(0);
|
||||
return (
|
||||
<Group
|
||||
gap={0}
|
||||
@@ -51,7 +54,7 @@ function ThemeCard({
|
||||
backgroundColor: cardTheme.backgroundColor,
|
||||
border: `solid ${cardTheme.borderColor}`,
|
||||
}}
|
||||
data-cardTheme={cardTheme.name}
|
||||
// data-cardTheme={cardTheme.name}
|
||||
onClick={() => {
|
||||
setValue(cardTheme.name);
|
||||
}}
|
||||
@@ -145,23 +148,46 @@ function ThemeCard({
|
||||
</Button>
|
||||
{currentTheme === 'random' && cardTheme.name === theme.name ? (
|
||||
<Group
|
||||
onMouseEnter={() => {
|
||||
setShift(2);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShift(0);
|
||||
}}
|
||||
onClick={() => {
|
||||
setShift(0);
|
||||
}}
|
||||
gap={0}
|
||||
style={{
|
||||
zIndex: 300,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
backgroundColor: 'white',
|
||||
top: 27 - shift,
|
||||
backgroundColor: shift === 0 ? 'white' : 'var(--color-background)',
|
||||
borderRadius: '50%',
|
||||
padding: '8px',
|
||||
margin: '4px',
|
||||
border: 'solid 2px black',
|
||||
border: 'solid 2px',
|
||||
borderColor: shift === 0 ? 'black' : 'var(--color-text-pri)',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<FiShuffle
|
||||
onClick={() => {
|
||||
const enabled = allThemes
|
||||
.filter((t: any) => t.randomEnabled)
|
||||
.filter((t: any) => t.name !== theme.name);
|
||||
if (enabled.length === 0) {
|
||||
setTheme(allThemes[0]);
|
||||
} else {
|
||||
const index = Math.floor(Math.random() * enabled.length);
|
||||
setTheme(enabled[index]);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
fontSize: '2em',
|
||||
fontWeight: 'bold',
|
||||
color: 'black',
|
||||
color: shift === 0 ? 'black' : 'var(--color-text-pri)',
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { modals } from '@mantine/modals';
|
||||
import { buttonClasses, textClasses } from 'styles';
|
||||
import ThemeCard from './ThemeCard';
|
||||
import ThemeModal from './ThemeModal';
|
||||
function ThemesSettings({ theme }: any) {
|
||||
function ThemesSettings({ theme, setTheme, allThemes }: any) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [pickerOpen, { open: openPicker, close: closePicker }] = useDisclosure(false);
|
||||
const [colorId, setColorId] = useState<string>('');
|
||||
@@ -18,23 +18,14 @@ function ThemesSettings({ theme }: any) {
|
||||
key: 'theme',
|
||||
defaultValue: 'blackboard',
|
||||
});
|
||||
const { data, isFetched } = useQueryAPI({
|
||||
endpoint: 'api/themes',
|
||||
});
|
||||
const { data: backgrounds, isFetched: isBackgroundFetched } = useQueryAPI({
|
||||
endpoint: 'api/backgrounds',
|
||||
});
|
||||
const [themesList, setThemesList] = useState([]);
|
||||
const [backgroundList, setBackgroundList] = useState<any>(['None']);
|
||||
const [selectedTheme, setSelectedTheme] = useState<any>({});
|
||||
const { mutateAsync: createTheme } = useMutationAPI();
|
||||
const { mutateAsync: deleteTheme } = useMutationAPI();
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetched) {
|
||||
setThemesList(data);
|
||||
}
|
||||
}, [data, isFetched]);
|
||||
const isMobile = window.matchMedia('(max-width: 600px)').matches;
|
||||
|
||||
useEffect(() => {
|
||||
if (backgrounds && isBackgroundFetched) {
|
||||
@@ -128,7 +119,7 @@ function ThemesSettings({ theme }: any) {
|
||||
color={theme.primaryColor}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify='center' pos={'relative'}>
|
||||
<Group justify='center' align='center' pos={'relative'}>
|
||||
<Overlay
|
||||
display={currentTheme === 'random' ? 'flex' : 'none'}
|
||||
blur={5}
|
||||
@@ -136,10 +127,12 @@ function ThemesSettings({ theme }: any) {
|
||||
borderRadius: '10px',
|
||||
marginTop: '-15px',
|
||||
marginBottom: '-10px',
|
||||
marginRight: isMobile ? '0' : '50px',
|
||||
marginLeft: isMobile ? '0' : '50px',
|
||||
}}
|
||||
pos={'absolute'}
|
||||
/>
|
||||
{themesList.map((cardTheme: any) => (
|
||||
{allThemes?.map((cardTheme: any) => (
|
||||
<Group key={cardTheme.name} pos={'relative'}>
|
||||
{cardTheme.name === currentTheme && (
|
||||
<FiChevronRight
|
||||
@@ -166,6 +159,8 @@ function ThemesSettings({ theme }: any) {
|
||||
>
|
||||
<ThemeCard
|
||||
theme={theme}
|
||||
allThemes={allThemes}
|
||||
setTheme={setTheme}
|
||||
cardTheme={cardTheme}
|
||||
currentTheme={currentTheme}
|
||||
handleOpen={handleOpen}
|
||||
|
||||
Reference in New Issue
Block a user