Redesign
This commit is contained in:
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
|
||||
dist
|
||||
.solid
|
||||
.output
|
||||
@@ -10,19 +9,11 @@ netlify
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
data/
|
||||
|
||||
8
docker/Dockerfile
Normal file
8
docker/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app/frontend
|
||||
COPY package.json .
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
@@ -1,25 +0,0 @@
|
||||
import { groupBy } from 'ramda';
|
||||
import { AppDefinition } from '~/types';
|
||||
import tagOrder from '../../data/tag-order.json';
|
||||
import { Pill } from './Pill';
|
||||
|
||||
import style from './AppList.module.css';
|
||||
|
||||
export function AppList({ apps }: { apps: AppDefinition[] }) {
|
||||
const grouped = groupBy((app) => app.tags, apps);
|
||||
|
||||
return (
|
||||
<div class={style.AppListWrap}>
|
||||
{tagOrder.map((tag) => (
|
||||
<div>
|
||||
<h2 class={style.Header}>{tag}</h2>
|
||||
<div class={style.AppList}>
|
||||
{grouped[tag]!.map((app, i) => (
|
||||
<Pill {...app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.Box {
|
||||
background-color: rgba(30, 30, 30, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(30, 30, 30, 1);
|
||||
color: var(--color-text);
|
||||
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.Hoverable:hover, .Hoverable:focus-within {
|
||||
background-color: rgba(50, 50, 50, 0.85);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.Date {
|
||||
font-family: nova mono;
|
||||
font-size: 3rem;
|
||||
letter-spacing: -2px;
|
||||
text-shadow: 0 0 4px #000;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { getDate } from '~/lib/date';
|
||||
|
||||
import style from './Date.module.css';
|
||||
|
||||
export function DateView() {
|
||||
return (
|
||||
<>
|
||||
<h1 class={style.Date}>{getDate()}</h1>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.Logo {
|
||||
font-family: "Nova Mono";
|
||||
color: #aaa;
|
||||
text-shadow: 1px 1px 1px #000;
|
||||
letter-spacing: -2px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import style from './Logo.module.css';
|
||||
|
||||
export function Logo() {
|
||||
return <h1 class={style.Logo}>gar.dev</h1>;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.Search {
|
||||
padding: 10px 20px;
|
||||
width: 300px;
|
||||
border: 0;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(30, 30, 30, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(30, 30, 30, 1);
|
||||
color: var(--color-text);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.Search:focus-within {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.Search:focus-within, .Search:hover {
|
||||
background-color: rgba(50, 50, 50, 0.85);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Checkbox,
|
||||
ModalFooter,
|
||||
Button,
|
||||
Flex,
|
||||
} from '@hope-ui/solid';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
|
||||
type SettingsModalProps = {
|
||||
opened: boolean;
|
||||
onClose(): void;
|
||||
};
|
||||
|
||||
export function SettingsModal(props: SettingsModalProps) {
|
||||
const { appSettings, setAppSettings } = useSettings();
|
||||
|
||||
return (
|
||||
<Modal opened={props.opened} onClose={props.onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalHeader>Settings</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex direction="column" gap=".5rem">
|
||||
<Checkbox
|
||||
checked={appSettings.showLocations}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, showLocations: (e.target as any).checked }))
|
||||
}
|
||||
>
|
||||
Show locations
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={appSettings.viewAsTable}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, viewAsTable: (e.target as any).checked }))
|
||||
}
|
||||
>
|
||||
Table View
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={props.onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,19 @@
|
||||
}
|
||||
|
||||
.Header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 8px;
|
||||
font-size: 2rem;
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
text-shadow: 0 0 4px #000;
|
||||
text-shadow: 1px 1px 5px #000;
|
||||
}
|
||||
|
||||
.AppList {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: .5rem;
|
||||
margin: 0 auto 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
50
src/components/apps/AppList.tsx
Normal file
50
src/components/apps/AppList.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { groupBy } from 'ramda';
|
||||
import { AppDefinition } from '~/types';
|
||||
import tagOrder from '../../../data/tag-order.json';
|
||||
import { Pill } from './Pill';
|
||||
|
||||
import { Accessor } from 'solid-js';
|
||||
import style from './AppList.module.css';
|
||||
|
||||
export function AppList({
|
||||
apps,
|
||||
tag,
|
||||
resetTag,
|
||||
}: {
|
||||
apps: AppDefinition[];
|
||||
tag?: Accessor<string | null>;
|
||||
resetTag?(): void;
|
||||
}) {
|
||||
const grouped = groupBy((app) => app.tags, apps);
|
||||
|
||||
return (
|
||||
<div class={style.AppListWrap}>
|
||||
{tag?.() ? (
|
||||
<div>
|
||||
<h2 class={style.Header}>
|
||||
{tag()}
|
||||
<span style={{ cursor: 'pointer' }} onClick={resetTag}>
|
||||
×
|
||||
</span>
|
||||
</h2>
|
||||
<div class={style.AppList}>
|
||||
{grouped[tag()!]!.map((app, i) => (
|
||||
<Pill {...app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
tagOrder.map((tag) => (
|
||||
<div>
|
||||
<h2 class={style.Header}>{tag}</h2>
|
||||
<div class={style.AppList}>
|
||||
{grouped[tag]!.map((app, i) => (
|
||||
<Pill {...app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Anchor, Tag, TagProps } from '@hope-ui/solid';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
import { AppDefinition } from '~/types';
|
||||
import tagOrder from '../../data/tag-order.json';
|
||||
import tagOrder from '../../../data/tag-order.json';
|
||||
import { getAppIcon } from '~/api/get-app-icon';
|
||||
|
||||
import style from './AppTable.module.css';
|
||||
@@ -10,18 +10,15 @@
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
padding: 20px;
|
||||
font-size: 1.2rem;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
padding: 1rem;
|
||||
text-decoration: none;
|
||||
border: 2px solid rgba(30, 30, 30, 1);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.Icon {
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
width: 36px;
|
||||
max-height: 54px;
|
||||
}
|
||||
|
||||
.Url {
|
||||
@@ -38,14 +35,15 @@
|
||||
|
||||
.Name {
|
||||
line-height: 1.3rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Index {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
font-size: 120px;
|
||||
color: rgba(90, 90, 90, 1);
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
font-size: 100px;
|
||||
color: rgba(90, 90, 90, 0.3);
|
||||
letter-spacing: -4px;
|
||||
font-family: "Nova Mono";
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getAppIcon } from '~/api/get-app-icon';
|
||||
import { AppDefinition } from '~/types';
|
||||
import { Box } from './Box';
|
||||
import { Box } from '../ui/Box';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
|
||||
import style from './Pill.module.css';
|
||||
12
src/components/ui/Box.module.css
Normal file
12
src/components/ui/Box.module.css
Normal file
@@ -0,0 +1,12 @@
|
||||
.Box {
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #333;
|
||||
border-radius: 1rem;
|
||||
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.Hoverable:hover, .Hoverable:focus-within {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
42
src/components/ui/Sidebar.module.css
Normal file
42
src/components/ui/Sidebar.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.Sidebar {
|
||||
height: 100vh;
|
||||
width: max(25vw, 180px);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.Menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
gap: max(1vw, .5rem);
|
||||
font-size: max(1.8vw, 1.3rem);
|
||||
}
|
||||
|
||||
.Menu li {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
text-shadow: 1px 1px 5px #000;
|
||||
padding: max(1vw, .5rem) 1.5rem;
|
||||
}
|
||||
|
||||
.Menu li::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 0;
|
||||
width: 3px;
|
||||
background-color: var(--color-text);
|
||||
border-radius: 1rem;
|
||||
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
|
||||
.Menu li:hover::after {
|
||||
height: 100%;
|
||||
}
|
||||
24
src/components/ui/Sidebar.tsx
Normal file
24
src/components/ui/Sidebar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import tagOrder from '../../../data/tag-order.json';
|
||||
|
||||
import style from './Sidebar.module.css';
|
||||
|
||||
type SidebarProps = {
|
||||
onSelectTag?(tag: string): void;
|
||||
};
|
||||
|
||||
export function Sidebar(props: SidebarProps) {
|
||||
return (
|
||||
<div class={style.Sidebar}>
|
||||
<ul class={style.Menu}>
|
||||
{tagOrder.map((tag) => (
|
||||
<li
|
||||
onClick={() => props.onSelectTag?.(tag)}
|
||||
onMouseEnter={() => props.onSelectTag?.(tag)}
|
||||
>
|
||||
{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/components/widgets/Date.module.css
Normal file
23
src/components/widgets/Date.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.DateWidget {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-family: nova mono;
|
||||
letter-spacing: -2px;
|
||||
text-shadow: 1px 1px 7px #000;
|
||||
}
|
||||
|
||||
.Date {
|
||||
font-size: max(3vw, 2.5rem);
|
||||
margin-bottom: max(1vw, 1rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.Time {
|
||||
font-size: max(1.5vw, 1.5rem);
|
||||
text-shadow: 1px 1px 4px #000;
|
||||
}
|
||||
|
||||
.Logo {
|
||||
color: #ccc;
|
||||
}
|
||||
21
src/components/widgets/Date.tsx
Normal file
21
src/components/widgets/Date.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { getDate, getTime } from '~/lib/date';
|
||||
|
||||
import style from './Date.module.css';
|
||||
import { createEffect, createSignal } from 'solid-js';
|
||||
|
||||
export function DateView() {
|
||||
const [time, setTime] = createSignal(getTime());
|
||||
|
||||
createEffect(() => {
|
||||
setInterval(() => setTime(getTime()), 1000);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={style.DateWidget}>
|
||||
<h1 class={style.Date}>{getDate()}</h1>
|
||||
<h3 class={style.Time}>
|
||||
{time()} · <span class={style.Logo}>gar.dev</span>
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
src/components/widgets/Search.module.css
Normal file
26
src/components/widgets/Search.module.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.Search {
|
||||
padding: 10px 20px;
|
||||
width: max(40vw, 300px);
|
||||
border: 0;
|
||||
font-size: 1rem;
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.2);
|
||||
color: var(--color-text);
|
||||
|
||||
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.Search::placeholder {
|
||||
color: var(--color-text);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.Search:focus-within {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.Search:focus-within, .Search:hover {
|
||||
border-color: var(--color-text);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
57
src/components/widgets/Settings.tsx
Normal file
57
src/components/widgets/Settings.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Checkbox,
|
||||
ModalFooter,
|
||||
Button,
|
||||
Flex,
|
||||
createDisclosure,
|
||||
} from '@hope-ui/solid';
|
||||
import { HiSolidCog } from 'solid-icons/hi';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
|
||||
export function Settings() {
|
||||
const { appSettings, setAppSettings } = useSettings();
|
||||
const { isOpen, onOpen, onClose } = createDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<HiSolidCog onClick={onOpen} size={24} />
|
||||
|
||||
<Modal opened={isOpen()} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalHeader>Settings</ModalHeader>
|
||||
<ModalBody>
|
||||
<Flex direction="column" gap=".5rem">
|
||||
<Checkbox
|
||||
checked={appSettings.showLocations}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, showLocations: (e.target as any).checked }))
|
||||
}
|
||||
>
|
||||
Show locations
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={appSettings.viewAsTable}
|
||||
onChange={(e) =>
|
||||
setAppSettings((prev) => ({ ...prev, viewAsTable: (e.target as any).checked }))
|
||||
}
|
||||
>
|
||||
Table View
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
flex-grow: 1;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
padding: 24px;
|
||||
height: 200px;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
@@ -14,7 +15,7 @@
|
||||
gap: .5rem;
|
||||
margin-left: .5rem;
|
||||
font-size: 1.3rem;
|
||||
color: var(--color-bright);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.FeelsLike :last-child {
|
||||
@@ -23,7 +24,7 @@
|
||||
}
|
||||
|
||||
.Delimiter {
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 1.2rem;
|
||||
margin-bottom: .8rem;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { createWeather } from '~/api/open-weather';
|
||||
import { getTime } from '~/lib/date';
|
||||
|
||||
import style from './Weather.module.css';
|
||||
import { Box } from './Box';
|
||||
import { Box } from '../ui/Box';
|
||||
|
||||
export function Weather() {
|
||||
const weather = createWeather();
|
||||
@@ -25,8 +25,8 @@ export function Weather() {
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.WeatherLine}>
|
||||
<HiSolidArrowDown color="lightcoral" />
|
||||
{weather()?.temp_min}° <HiSolidArrowUp color="lightgreen" /> {weather()?.temp_max}
|
||||
<HiSolidArrowDown color="darkred" />
|
||||
{weather()?.temp_min}° <HiSolidArrowUp color="darkgreen" /> {weather()?.temp_max}
|
||||
°
|
||||
<WiHumidity />
|
||||
{weather()?.humidity}%<br />
|
||||
@@ -1,6 +1,5 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export const getTime = (ts: number) =>
|
||||
new Date(ts).toTimeString().split(' ')[0].split(':').slice(0, 2).join(':');
|
||||
|
||||
export const getDate = () => format(new Date(), 'EEEE, dd MMMM yyyy');
|
||||
|
||||
export const getTime = () => format(new Date(), 'HH:mm:ss');
|
||||
|
||||
10
src/root.css
10
src/root.css
@@ -1,6 +1,6 @@
|
||||
:root {
|
||||
--color-text: #bbb;
|
||||
--color-secondary: #777;
|
||||
--color-text: #ddd;
|
||||
--color-secondary: #555;
|
||||
--color-bright: #eee;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ main {
|
||||
}
|
||||
|
||||
main {
|
||||
width: 90vw;
|
||||
max-width: 1394px;
|
||||
/* width: 90vw;
|
||||
max-width: 1394px; */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
/* padding: 20px; */
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -16,6 +16,7 @@ import './styles/weather-icons.min.css';
|
||||
import './styles/reset.css';
|
||||
import './root.css';
|
||||
import { SettingsProvider } from './state/SettingsProvider';
|
||||
import { Background } from './components/ui/Background';
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
@@ -34,6 +35,7 @@ export default function Root() {
|
||||
<Body>
|
||||
<HopeProvider config={{ initialColorMode: 'dark' }}>
|
||||
<SettingsProvider>
|
||||
<Background />
|
||||
<div class="scroll-wrapper">
|
||||
<Suspense>
|
||||
<ErrorBoundary>
|
||||
|
||||
21
src/routes/index.module.css
Normal file
21
src/routes/index.module.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.SidebarPage {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ContentWrapper {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
overflow: scroll;
|
||||
|
||||
padding: 0 max(10vw, 2rem);
|
||||
}
|
||||
|
||||
.Widgets {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
@@ -1,84 +1,78 @@
|
||||
import { createDisclosure } from '@hope-ui/solid';
|
||||
import { HiSolidCog } from 'solid-icons/hi';
|
||||
import { onCleanup, onMount } from 'solid-js';
|
||||
import { Title } from 'solid-start';
|
||||
import { AppList } from '~/components/AppList';
|
||||
import { AppTable } from '~/components/AppTable';
|
||||
import { Background } from '~/components/Background';
|
||||
import { DateView } from '~/components/Date';
|
||||
import { Logo } from '~/components/Logo';
|
||||
import { Search } from '~/components/Search';
|
||||
import { SettingsModal } from '~/components/SettingsModal';
|
||||
import { Weather } from '~/components/Weather';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { DateView } from '~/components/widgets/Date';
|
||||
import { Search } from '~/components/widgets/Search';
|
||||
import { Weather } from '~/components/widgets/Weather';
|
||||
import data from '../../data/apps.json';
|
||||
import { Sidebar } from '../components/ui/Sidebar';
|
||||
|
||||
function handleKeypress(e: KeyboardEvent) {
|
||||
if (e.key === '/') {
|
||||
document.getElementById('search')?.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const app = data.find((app) => app.shortcut === e.key);
|
||||
|
||||
if (app) {
|
||||
window.open(app.url);
|
||||
}
|
||||
}
|
||||
import { AppList } from '~/components/apps/AppList';
|
||||
import style from './index.module.css';
|
||||
import { HiOutlineXCircle, HiSolidXCircle } from 'solid-icons/hi';
|
||||
|
||||
export default function Home() {
|
||||
const { appSettings } = useSettings();
|
||||
const {
|
||||
isOpen: isSettingsOpen,
|
||||
onOpen: openSettings,
|
||||
onClose: closeSettings,
|
||||
} = createDisclosure();
|
||||
|
||||
onMount(() => {
|
||||
parent.addEventListener('keypress', handleKeypress);
|
||||
parent.addEventListener('keydown', handleKeypress);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
parent.removeEventListener('keypress', handleKeypress);
|
||||
parent.removeEventListener('keydown', handleKeypress);
|
||||
});
|
||||
|
||||
const [activeTag, setActiveTag] = createSignal<string | null>(null);
|
||||
const [timeout, storeTimeout] = createSignal<NodeJS.Timeout | null>(null);
|
||||
|
||||
const switchToTag = (tag: string) => {
|
||||
setActiveTag(tag);
|
||||
clearTimeout(timeout() as NodeJS.Timeout);
|
||||
|
||||
storeTimeout(setTimeout(() => setActiveTag(null), 3000));
|
||||
};
|
||||
|
||||
const resetTag = () => {
|
||||
setActiveTag(null);
|
||||
clearTimeout(timeout() as NodeJS.Timeout);
|
||||
};
|
||||
|
||||
function handleKeypress(e: KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'Escape': {
|
||||
document.getElementById('search')?.blur();
|
||||
resetTag();
|
||||
break;
|
||||
}
|
||||
case '/': {
|
||||
document.getElementById('search')?.focus();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (document.activeElement?.id !== 'search') {
|
||||
const app = data.find((app) => app.shortcut === e.key);
|
||||
|
||||
if (app) {
|
||||
window.open(app.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Title>gar.dev</Title>
|
||||
<Background />
|
||||
<HiSolidCog onClick={openSettings} size={24} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
'flex-wrap': 'wrap',
|
||||
gap: '2rem',
|
||||
'margin-top': '2rem',
|
||||
'justify-content': 'space-between',
|
||||
'align-items': 'center',
|
||||
'margin-bottom': appSettings.viewAsTable ? '20px' : '80px',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
'align-items': 'center',
|
||||
'margin-bottom': '2rem',
|
||||
gap: '2rem',
|
||||
'flex-wrap': 'wrap',
|
||||
}}
|
||||
>
|
||||
<DateView />
|
||||
<Logo />
|
||||
</div>
|
||||
<Search />
|
||||
<main class={style.SidebarPage}>
|
||||
<Sidebar onSelectTag={switchToTag} />
|
||||
<div class={style.ContentWrapper}>
|
||||
<div class={style.Widgets}>
|
||||
{activeTag() ? (
|
||||
<AppList apps={data} tag={activeTag} resetTag={resetTag} />
|
||||
) : (
|
||||
<>
|
||||
<Search />
|
||||
<DateView />
|
||||
<Weather />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Weather />
|
||||
</div>
|
||||
|
||||
{appSettings.viewAsTable ? <AppTable apps={data} /> : <AppList apps={data} />}
|
||||
|
||||
<SettingsModal opened={isSettingsOpen()} onClose={closeSettings} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user