Add table view
This commit is contained in:
52
package-lock.json
generated
52
package-lock.json
generated
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "vertex",
|
"name": "vertex",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vertex",
|
"name": "vertex",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.16.3",
|
"@mantine/core": "^7.16.3",
|
||||||
"@mantine/hooks": "^7.16.3",
|
"@mantine/hooks": "^7.16.3",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router": "^7.1.5",
|
||||||
"reset.css": "^2.0.2",
|
"reset.css": "^2.0.2",
|
||||||
"unsplash-js": "^7.0.19"
|
"unsplash-js": "^7.0.19"
|
||||||
},
|
},
|
||||||
@@ -1414,6 +1416,11 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@@ -1896,6 +1903,14 @@
|
|||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3010,6 +3025,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz",
|
||||||
|
"integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cookie": "^0.6.0",
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0",
|
||||||
|
"turbo-stream": "2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@@ -3151,6 +3189,11 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -3264,6 +3307,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/turbo-stream": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Vertex – A start page for selfhosted services.",
|
"description": "Vertex – A start page for selfhosted services.",
|
||||||
"keywords": ["startpage", "selfhosted", "dashboard", "vertex"],
|
"keywords": [
|
||||||
|
"startpage",
|
||||||
|
"selfhosted",
|
||||||
|
"dashboard",
|
||||||
|
"vertex"
|
||||||
|
],
|
||||||
"author": "Georgi Gardev <georgi@gar.dev>",
|
"author": "Georgi Gardev <georgi@gar.dev>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -21,6 +26,7 @@
|
|||||||
"jotai": "^2.11.3",
|
"jotai": "^2.11.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router": "^7.1.5",
|
||||||
"reset.css": "^2.0.2",
|
"reset.css": "^2.0.2",
|
||||||
"unsplash-js": "^7.0.19"
|
"unsplash-js": "^7.0.19"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core';
|
||||||
|
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||||
import { Background as BackgroundComponent } from '~/components/ui/background';
|
import { Background as BackgroundComponent } from '~/components/ui/background';
|
||||||
import { useLoadBackgrounds } from './hooks/use-load-backgrounds';
|
import { useLoadBackgrounds } from './hooks/use-load-backgrounds';
|
||||||
import { IndexPage } from './pages/index-page';
|
import { IndexPage } from './pages/index-page';
|
||||||
@@ -9,7 +10,11 @@ export function App() {
|
|||||||
return (
|
return (
|
||||||
<MantineProvider defaultColorScheme="dark">
|
<MantineProvider defaultColorScheme="dark">
|
||||||
<BackgroundComponent />
|
<BackgroundComponent />
|
||||||
<IndexPage />
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<IndexPage />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type Settings = {
|
|||||||
viewAsTable: boolean;
|
viewAsTable: boolean;
|
||||||
useSavedBackgrounds: boolean;
|
useSavedBackgrounds: boolean;
|
||||||
unsplashQuery: string;
|
unsplashQuery: string;
|
||||||
viewMode: 'tile' | 'pill';
|
viewMode: 'tile' | 'pill' | 'table';
|
||||||
backgroundBlur: number;
|
backgroundBlur: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AppButton } from './app-button';
|
|||||||
import { AppPill } from './app-pill';
|
import { AppPill } from './app-pill';
|
||||||
|
|
||||||
import style from './app-list.module.css';
|
import style from './app-list.module.css';
|
||||||
|
import { AppsTable } from './apps-table';
|
||||||
|
|
||||||
type AppListProps = {
|
type AppListProps = {
|
||||||
category: string;
|
category: string;
|
||||||
@@ -27,11 +28,15 @@ export function AppList({ category, apps, onReset }: AppListProps) {
|
|||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<div className={`${style.AppList} ${viewMode === 'tile' ? style.Tile : style.Pill}`}>
|
{viewMode === 'table' ? (
|
||||||
{apps.length > 0
|
<AppsTable apps={apps} />
|
||||||
? apps.map((app) => <Component key={app.url} {...app} />)
|
) : (
|
||||||
: 'No apps found'}
|
<div className={`${style.AppList} ${viewMode === 'tile' ? style.Tile : style.Pill}`}>
|
||||||
</div>
|
{apps.length > 0
|
||||||
|
? apps.map((app) => <Component key={app.url} {...app} />)
|
||||||
|
: 'No apps found'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/components/apps/apps-table.module.css
Normal file
6
src/components/apps/apps-table.module.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.Table {
|
||||||
|
background-color: var(--color-bg-dark);
|
||||||
|
backdrop-filter: var(--bg-blur);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
}
|
||||||
99
src/components/apps/apps-table.tsx
Normal file
99
src/components/apps/apps-table.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Anchor, Flex, Table } from '@mantine/core';
|
||||||
|
import { IconSortAscending } from '@tabler/icons-react';
|
||||||
|
import { JSX, useMemo, useState } from 'react';
|
||||||
|
import { getAppIcon } from '~/api/get-app-icon';
|
||||||
|
import { AppDefinition } from '~/types';
|
||||||
|
|
||||||
|
import style from './apps-table.module.css';
|
||||||
|
|
||||||
|
type AppsTableProps = {
|
||||||
|
apps: AppDefinition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AppsTable({ apps }: AppsTableProps): JSX.Element {
|
||||||
|
const [sortBy, setSortBy] = useState<'name' | 'url' | 'location' | 'shortcut'>('location');
|
||||||
|
|
||||||
|
const tableData = useMemo(
|
||||||
|
() =>
|
||||||
|
apps
|
||||||
|
.map((app) => ({
|
||||||
|
...app,
|
||||||
|
shortcut: app.shortcut ?? '',
|
||||||
|
}))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const [serverA, portA] = a.location?.split(':') ?? ['', ''];
|
||||||
|
const [serverB, portB] = b.location?.split(':') ?? ['', ''];
|
||||||
|
const portCmp = Number(portA) - Number(portB);
|
||||||
|
|
||||||
|
if (!a[sortBy]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!b[sortBy]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy === 'location') {
|
||||||
|
return serverA.localeCompare(serverB) || portCmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a[sortBy].localeCompare(b[sortBy]);
|
||||||
|
}),
|
||||||
|
[apps, sortBy]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table mt="xl" stickyHeader className={style.Table}>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
Name <IconSortAscending size={16} onClick={() => setSortBy('name')} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
URL <IconSortAscending size={16} onClick={() => setSortBy('url')} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
Location <IconSortAscending size={16} onClick={() => setSortBy('location')} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Th>
|
||||||
|
<Table.Th>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
Shortcut <IconSortAscending size={16} onClick={() => setSortBy('shortcut')} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{tableData.map((app) => (
|
||||||
|
<Table.Tr key={app.name}>
|
||||||
|
<Table.Td>
|
||||||
|
<Anchor href={app.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
{app.icon && (
|
||||||
|
<img
|
||||||
|
src={getAppIcon(app.icon, app.icon_source)}
|
||||||
|
alt={app.name}
|
||||||
|
style={{ width: '16px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{app.name}
|
||||||
|
</Flex>
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Anchor href={app.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{app.url}
|
||||||
|
</Anchor>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{app.location}</Table.Td>
|
||||||
|
<Table.Td>{app.shortcut}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/components/ui/layout.module.css
Normal file
19
src/components/ui/layout.module.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.SidebarPage {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.SidebarPage {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ContentWrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2rem;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 0 clamp(0.6rem, 5vw, 10rem);
|
||||||
|
}
|
||||||
28
src/components/ui/layout.tsx
Normal file
28
src/components/ui/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { JSX, ReactNode } from 'react';
|
||||||
|
import { Sidebar, SidebarProps } from './sidebar';
|
||||||
|
|
||||||
|
import style from './layout.module.css';
|
||||||
|
|
||||||
|
type LayoutProps = {
|
||||||
|
isLoading: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
} & SidebarProps;
|
||||||
|
|
||||||
|
export function Layout({
|
||||||
|
isLoading,
|
||||||
|
categories,
|
||||||
|
activeCategory,
|
||||||
|
selectCategory,
|
||||||
|
children,
|
||||||
|
}: LayoutProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<main className={`${style.SidebarPage} ${isLoading ? style.Loading : ''}`}>
|
||||||
|
<Sidebar
|
||||||
|
categories={categories}
|
||||||
|
selectCategory={selectCategory}
|
||||||
|
activeCategory={activeCategory}
|
||||||
|
/>
|
||||||
|
<div className={style.ContentWrapper}>{children}</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { WeatherWidget } from '../widgets/weather-widget';
|
|||||||
import { useLoadingTimeout } from '~/hooks/use-loading-timeout';
|
import { useLoadingTimeout } from '~/hooks/use-loading-timeout';
|
||||||
import style from './sidebar.module.css';
|
import style from './sidebar.module.css';
|
||||||
|
|
||||||
type SidebarProps = {
|
export type SidebarProps = {
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
activeCategory?: string | null;
|
activeCategory?: string | null;
|
||||||
selectCategory: (category: string, mode: 'permanent' | 'temporary') => void;
|
selectCategory: (category: string, mode: 'permanent' | 'temporary') => void;
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ export function Settings({ className }: SettingsProps) {
|
|||||||
w="30%"
|
w="30%"
|
||||||
value={appSettings.viewMode}
|
value={appSettings.viewMode}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setAppSettings((prev) => ({ ...prev, viewMode: value as 'tile' | 'pill' }))
|
setAppSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
viewMode: value as 'tile' | 'pill' | 'table',
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
data={[
|
data={[
|
||||||
{ label: 'Tiles', value: 'tile' },
|
{ label: 'Tiles', value: 'tile' },
|
||||||
{ label: 'Pills', value: 'pill' },
|
{ label: 'Pills', value: 'pill' },
|
||||||
|
{ label: 'Table', value: 'table' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { useCategories } from '~/api/get-apps';
|
|||||||
import { useSearchProviders } from '~/api/get-search-providers';
|
import { useSearchProviders } from '~/api/get-search-providers';
|
||||||
import { onNextBackgroundAtom, settingsAtom } from '~/atoms';
|
import { onNextBackgroundAtom, settingsAtom } from '~/atoms';
|
||||||
import { AppList } from '~/components/apps/app-list';
|
import { AppList } from '~/components/apps/app-list';
|
||||||
import { Sidebar } from '~/components/ui/sidebar';
|
|
||||||
import { CurrentImageWidget } from '~/components/widgets/current-image-widget';
|
import { CurrentImageWidget } from '~/components/widgets/current-image-widget';
|
||||||
import { DateWidget } from '~/components/widgets/date-widget';
|
import { DateWidget } from '~/components/widgets/date-widget';
|
||||||
import { SearchWidget } from '~/components/widgets/search-widget';
|
import { SearchWidget } from '~/components/widgets/search-widget';
|
||||||
import { useActiveCategory } from '~/hooks/use-active-category';
|
import { useActiveCategory } from '~/hooks/use-active-category';
|
||||||
import { useLoadingTimeout } from '~/hooks/use-loading-timeout';
|
import { useLoadingTimeout } from '~/hooks/use-loading-timeout';
|
||||||
|
|
||||||
|
import { Layout } from '~/components/ui/layout';
|
||||||
import style from './index-page.module.css';
|
import style from './index-page.module.css';
|
||||||
|
|
||||||
export function IndexPage() {
|
export function IndexPage() {
|
||||||
@@ -73,46 +73,44 @@ export function IndexPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={`${style.SidebarPage} ${isInitialLoading ? style.Loading : ''}`}>
|
<Layout
|
||||||
<Sidebar
|
isLoading={isInitialLoading}
|
||||||
categories={categories?.flatMap((c) => c.name)}
|
categories={categories.flatMap((c) => c.name)}
|
||||||
selectCategory={selectCategory}
|
activeCategory={activeCategoryName}
|
||||||
activeCategory={activeCategoryName}
|
selectCategory={selectCategory}
|
||||||
/>
|
>
|
||||||
<div className={style.ContentWrapper}>
|
<div className={style.Widgets}>
|
||||||
<div className={style.Widgets}>
|
{activeCategory ? (
|
||||||
{activeCategory ? (
|
<AppList
|
||||||
<AppList
|
category={activeCategory.name}
|
||||||
category={activeCategory.name}
|
apps={activeCategory.apps}
|
||||||
apps={activeCategory.apps}
|
onReset={resetCategory}
|
||||||
onReset={resetCategory}
|
/>
|
||||||
/>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<div className={style.MainWidgets}>
|
||||||
<div className={style.MainWidgets}>
|
<DateWidget />
|
||||||
<DateWidget />
|
<SearchWidget
|
||||||
<SearchWidget
|
term={searchTerm}
|
||||||
term={searchTerm}
|
setTerm={setSearchTerm}
|
||||||
setTerm={setSearchTerm}
|
providers={searchProviders}
|
||||||
providers={searchProviders}
|
canOpenApp={searchResults.length > 0}
|
||||||
canOpenApp={searchResults.length > 0}
|
onOpenApp={() => window.open(searchResults[0].url)}
|
||||||
onOpenApp={() => window.open(searchResults[0].url)}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
{shouldSearchLocally ? (
|
||||||
{shouldSearchLocally ? (
|
<AppList
|
||||||
<AppList
|
category="search results"
|
||||||
category="search results"
|
apps={searchResults}
|
||||||
apps={searchResults}
|
onReset={() => setSearchTerm('')}
|
||||||
onReset={() => setSearchTerm('')}
|
/>
|
||||||
/>
|
) : (
|
||||||
) : (
|
<AppList category="" apps={apps} />
|
||||||
<AppList category="" apps={apps} />
|
)}
|
||||||
)}
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{showImageDetails && <CurrentImageWidget loading={isInitialLoading} />}
|
{showImageDetails && <CurrentImageWidget loading={isInitialLoading} />}
|
||||||
</main>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user