Archived
Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export const getAppIcon = (icon: string) =>
|
||||
`https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/${icon}.png`;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
import { WeatherData } from '~/types';
|
||||
import { Accessor } from 'solid-js';
|
||||
|
||||
export function fetchWeather(): Promise<WeatherData> {
|
||||
const lat = import.meta.env.VITE_OPEN_WEATHER_LAT;
|
||||
const lon = import.meta.env.VITE_OPEN_WEATHER_LON;
|
||||
const appId = import.meta.env.VITE_OPEN_WEATHER_KEY;
|
||||
return fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${appId}&units=metric`
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
return {
|
||||
...data.weather[0],
|
||||
...data.main,
|
||||
visibility: data.visibility,
|
||||
wind_speed: data.wind.speed,
|
||||
wind_deg: data.wind.deg,
|
||||
sunrise: data.sys.sunrise,
|
||||
sunset: data.sys.sunset,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const createWeather = (): Accessor<WeatherData | undefined> => {
|
||||
const [weather, setWeather] = createSignal<WeatherData>();
|
||||
fetchWeather().then(setWeather);
|
||||
|
||||
return weather;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApi } from 'unsplash-js';
|
||||
|
||||
const UNSPLASH_ACCESS_KEY = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;
|
||||
|
||||
export const unsplashApi = createApi({ accessKey: UNSPLASH_ACCESS_KEY, fetch });
|
||||
|
||||
export const getRandomBackground = () =>
|
||||
unsplashApi.photos.getRandom({ count: 10, collectionIds: ['114728'] });
|
||||
@@ -0,0 +1,20 @@
|
||||
.AppListWrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.Header {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 8px;
|
||||
font-size: 2rem;
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
text-shadow: 0 0 4px #000;
|
||||
}
|
||||
|
||||
.AppList {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin: 0 auto 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.Name {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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 { getAppIcon } from '~/api/get-app-icon';
|
||||
|
||||
import style from './AppTable.module.css';
|
||||
|
||||
export function AppTable({ apps }: { apps: AppDefinition[] }) {
|
||||
const { appSettings } = useSettings();
|
||||
|
||||
const TAG_TO_COLOR: Record<string, TagProps['colorScheme']> = {
|
||||
media: 'primary',
|
||||
productivity: 'accent',
|
||||
infra: 'neutral',
|
||||
'smart home': 'success',
|
||||
'*arr': 'danger',
|
||||
social: 'warning',
|
||||
};
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th>Tags</th>
|
||||
{appSettings.showLocations && <th>Location</th>}
|
||||
<th>Shortuct</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{apps
|
||||
.sort((tagA, tagB) => tagOrder.indexOf(tagA.tags) - tagOrder.indexOf(tagB.tags))
|
||||
.map((app) => (
|
||||
<tr>
|
||||
<td>
|
||||
<a href={app.url} target="_blank" rel="noopener noreferrer" class={style.Name}>
|
||||
<img class={style.Icon} src={getAppIcon(app.icon)} alt={app.name} />
|
||||
{app.name}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href={app.url} target="_blank" rel="noopener noreferrer">
|
||||
{app.url.split('://')[1]}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{app.tags.split(',').map((tag) => (
|
||||
<Tag colorScheme={TAG_TO_COLOR[tag]}>{tag}</Tag>
|
||||
))}
|
||||
</td>
|
||||
{appSettings.showLocations && <td>{app.location}</td>}
|
||||
<td>{app.shortcut}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.Background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-size: cover;
|
||||
z-index: -1;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
backdrop-filter: blur(10px);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Random } from 'unsplash-js/dist/methods/photos/types';
|
||||
import { createLocalStore } from '~/lib/create-local-store';
|
||||
import { random } from '~/lib/math';
|
||||
import { getRandomBackground, unsplashApi } from '~/api/unsplash';
|
||||
|
||||
import style from './Background.module.css';
|
||||
import { BACKGROUND_FETCH_MINS } from '~/config';
|
||||
import { createEffect, createMemo, createSignal } from 'solid-js';
|
||||
|
||||
type BackgroundStorage = {
|
||||
fetched_at: number;
|
||||
backgrounds: Random[] | null;
|
||||
};
|
||||
|
||||
export function Background() {
|
||||
const [backgrounds, setBackgrounds] = createLocalStore<BackgroundStorage>(
|
||||
'start-page-background',
|
||||
{
|
||||
fetched_at: 0,
|
||||
backgrounds: null,
|
||||
}
|
||||
);
|
||||
|
||||
const shouldFetch =
|
||||
!backgrounds?.backgrounds ||
|
||||
backgrounds.fetched_at < new Date().getTime() - 1000 * 60 * BACKGROUND_FETCH_MINS;
|
||||
|
||||
if (shouldFetch) {
|
||||
getRandomBackground().then((r) =>
|
||||
setBackgrounds({
|
||||
fetched_at: new Date().getTime(),
|
||||
backgrounds: (r?.response as Random[]) ?? null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const backgroundUrls = createMemo(() => {
|
||||
const backgroundLength = backgrounds?.backgrounds?.length ?? 0;
|
||||
const background = random(backgroundLength - 1);
|
||||
return backgrounds?.backgrounds?.[background].urls;
|
||||
});
|
||||
|
||||
const [fullLoaded, setFullLoaded] = createSignal(false);
|
||||
|
||||
createEffect(() => {
|
||||
if (!backgroundUrls()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var curImg = new Image();
|
||||
curImg.src = backgroundUrls()!.full;
|
||||
curImg.onload = () => setFullLoaded(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
class={style.Background}
|
||||
src={fullLoaded() ? backgroundUrls()?.full : backgroundUrls()?.thumb}
|
||||
/>
|
||||
{!fullLoaded() ? <div class={style.Overlay}></div> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { JSX } from 'solid-js';
|
||||
|
||||
import style from './Box.module.css';
|
||||
|
||||
type BoxProps = {
|
||||
children: JSX.Element;
|
||||
className?: string;
|
||||
disableHover?: boolean;
|
||||
};
|
||||
|
||||
export function Box(props: BoxProps) {
|
||||
return (
|
||||
<div
|
||||
classList={{
|
||||
[props.className ?? '']: Boolean(props.className),
|
||||
[style.Box]: true,
|
||||
[style.Hoverable]: !props.disableHover,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.Date {
|
||||
font-family: nova mono;
|
||||
font-size: 3rem;
|
||||
letter-spacing: -2px;
|
||||
text-shadow: 0 0 4px #000;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getDate } from '~/lib/date';
|
||||
|
||||
import style from './Date.module.css';
|
||||
|
||||
export function DateView() {
|
||||
return (
|
||||
<>
|
||||
<h1 class={style.Date}>{getDate()}</h1>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.Logo {
|
||||
font-family: "Nova Mono";
|
||||
color: #aaa;
|
||||
text-shadow: 1px 1px 1px #000;
|
||||
letter-spacing: -2px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import style from './Logo.module.css';
|
||||
|
||||
export function Logo() {
|
||||
return <h1 class={style.Logo}>gar.dev</h1>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
.Pill {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow-clip-margin: -10px;
|
||||
}
|
||||
|
||||
.Pill > a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
padding: 20px;
|
||||
font-size: 1.2rem;
|
||||
text-decoration: none;
|
||||
border: 2px solid rgba(30, 30, 30, 1);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.Icon {
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
|
||||
.Url {
|
||||
font-size: .85rem;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.Text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .3rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.Name {
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.Index {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
font-size: 120px;
|
||||
color: rgba(90, 90, 90, 1);
|
||||
letter-spacing: -4px;
|
||||
font-family: "Nova Mono";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getAppIcon } from '~/api/get-app-icon';
|
||||
import { AppDefinition } from '~/types';
|
||||
import { Box } from './Box';
|
||||
import { useSettings } from '~/state/SettingsProvider';
|
||||
|
||||
import style from './Pill.module.css';
|
||||
|
||||
export type PillProps = AppDefinition;
|
||||
|
||||
export function Pill({ url, name, icon, shortcut, location }: PillProps) {
|
||||
const { appSettings } = useSettings();
|
||||
|
||||
return (
|
||||
<Box className={style.Pill}>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<div class={style.Text}>
|
||||
<span class={style.Name}>{name}</span>
|
||||
<span class={style.Url}>{url.split('://')[1].split('/')[0]}</span>
|
||||
{appSettings.showLocations && location && <span class={style.Url}>{location}</span>}
|
||||
</div>
|
||||
<img class={style.Icon} src={getAppIcon(icon)} alt={name} />
|
||||
{shortcut && <span class={style.Index}>{shortcut}</span>}
|
||||
</a>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.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);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
import style from './Search.module.css';
|
||||
|
||||
export function Search() {
|
||||
const [term, setTerm] = createSignal('');
|
||||
|
||||
return (
|
||||
<input
|
||||
id="search"
|
||||
class={style.Search}
|
||||
type="text"
|
||||
placeholder="Search kagi..."
|
||||
value={term()}
|
||||
onInput={(e) => setTerm(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
window.open(`https://kagi.com/search?q=${term()}`, '_blank', 'noreferrer');
|
||||
setTerm('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
.Weather {
|
||||
flex-grow: 1;
|
||||
max-width: 300px;
|
||||
min-width: 200px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.FeelsLike {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
margin-left: .5rem;
|
||||
font-size: 1.3rem;
|
||||
color: var(--color-bright);
|
||||
}
|
||||
|
||||
.FeelsLike :last-child {
|
||||
font-size: 1rem;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.Delimiter {
|
||||
border-bottom: 1px solid var(--color-text);
|
||||
padding-bottom: 1.2rem;
|
||||
margin-bottom: .8rem;
|
||||
}
|
||||
|
||||
.WeatherLine {
|
||||
display: flex;
|
||||
gap: .3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.WeatherIcon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { HiSolidArrowDown, HiSolidArrowUp } from 'solid-icons/hi';
|
||||
import { WiHumidity, WiSunrise, WiSunset } from 'solid-icons/wi';
|
||||
import { createMemo } from 'solid-js';
|
||||
import { createWeather } from '~/api/open-weather';
|
||||
import { getTime } from '~/lib/date';
|
||||
|
||||
import style from './Weather.module.css';
|
||||
import { Box } from './Box';
|
||||
|
||||
export function Weather() {
|
||||
const weather = createWeather();
|
||||
|
||||
const sunrise = createMemo(() => getTime((weather()?.sunrise ?? 0) * 1000));
|
||||
const sunset = createMemo(() => getTime((weather()?.sunset ?? 0) * 1000));
|
||||
|
||||
return (
|
||||
<Box className={style.Weather} disableHover>
|
||||
{weather() ? (
|
||||
<>
|
||||
<div class={`${style.WeatherLine} ${style.Delimiter}`}>
|
||||
<i class={`wi wi-owm-${weather()?.id} ${style.WeatherIcon}`}></i>
|
||||
<div class={style.FeelsLike}>
|
||||
<span>{weather()?.feels_like}°</span>
|
||||
<span>{weather()?.main}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.WeatherLine}>
|
||||
<HiSolidArrowDown color="lightcoral" />
|
||||
{weather()?.temp_min}° <HiSolidArrowUp color="lightgreen" /> {weather()?.temp_max}
|
||||
°
|
||||
<WiHumidity />
|
||||
{weather()?.humidity}%<br />
|
||||
</div>
|
||||
<div class={style.WeatherLine}></div>
|
||||
<div class={style.WeatherLine}>
|
||||
<WiSunrise /> {sunrise()}
|
||||
<WiSunset /> {sunset()}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
'Loading'
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const BACKGROUND_FETCH_MINS = 60;
|
||||
@@ -0,0 +1,3 @@
|
||||
import { mount, StartClient } from "solid-start/entry-client";
|
||||
|
||||
mount(() => <StartClient />, document);
|
||||
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
createHandler,
|
||||
renderAsync,
|
||||
StartServer,
|
||||
} from "solid-start/entry-server";
|
||||
|
||||
export default createHandler(
|
||||
renderAsync((event) => <StartServer event={event} />)
|
||||
);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="solid-start/env" />
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createEffect } from 'solid-js';
|
||||
import { SetStoreFunction, Store, createStore } from 'solid-js/store';
|
||||
|
||||
export function createLocalStore<T extends object>(
|
||||
name: string,
|
||||
init: T
|
||||
): [Store<T>, SetStoreFunction<T>] {
|
||||
const localState = typeof localStorage === 'undefined' ? undefined : localStorage.getItem(name);
|
||||
const [state, setState] = createStore<T>(localState ? JSON.parse(localState) : init);
|
||||
|
||||
createEffect(() => {
|
||||
localStorage.setItem(name, JSON.stringify(state));
|
||||
});
|
||||
|
||||
return [state, setState];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
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');
|
||||
@@ -0,0 +1 @@
|
||||
export const random = (n: number) => Math.floor(Math.random() * n) + 1;
|
||||
@@ -0,0 +1,61 @@
|
||||
:root {
|
||||
--color-text: #bbb;
|
||||
--color-secondary: #777;
|
||||
--color-bright: #eee;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
html,
|
||||
main {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
main {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.scroll-wrapper {
|
||||
height: 100vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 90vw;
|
||||
max-width: 1394px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
background-color: rgba(30, 30, 30, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(30, 30, 30, 1);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// @refresh reload
|
||||
import { HopeProvider } from '@hope-ui/solid';
|
||||
import { Suspense } from 'solid-js';
|
||||
import {
|
||||
Body,
|
||||
ErrorBoundary,
|
||||
FileRoutes,
|
||||
Head,
|
||||
Html,
|
||||
Meta,
|
||||
Routes,
|
||||
Scripts,
|
||||
Title,
|
||||
} from 'solid-start';
|
||||
import './styles/weather-icons.min.css';
|
||||
import './styles/reset.css';
|
||||
import './root.css';
|
||||
import { SettingsProvider } from './state/SettingsProvider';
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<Title>gar.dev</Title>
|
||||
<Meta charset="utf-8" />
|
||||
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bad+Script&family=Fragment+Mono:ital@0;1&family=Nova+Mono&family=Raleway:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&family=Share+Tech+Mono&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</Head>
|
||||
<Body>
|
||||
<HopeProvider config={{ initialColorMode: 'dark' }}>
|
||||
<SettingsProvider>
|
||||
<div class="scroll-wrapper">
|
||||
<Suspense>
|
||||
<ErrorBoundary>
|
||||
<Routes>
|
||||
<FileRoutes />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
</div>
|
||||
</SettingsProvider>
|
||||
<Scripts />
|
||||
</HopeProvider>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Title } from "solid-start";
|
||||
import { HttpStatusCode } from "solid-start/server";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<h1>Page Not Found</h1>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build SolidStart apps.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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 data from '../../data/apps.json';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { appSettings } = useSettings();
|
||||
const {
|
||||
isOpen: isSettingsOpen,
|
||||
onOpen: openSettings,
|
||||
onClose: closeSettings,
|
||||
} = createDisclosure();
|
||||
|
||||
onMount(() => {
|
||||
parent.addEventListener('keypress', handleKeypress);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
parent.removeEventListener('keypress', handleKeypress);
|
||||
});
|
||||
|
||||
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 />
|
||||
</div>
|
||||
<Weather />
|
||||
</div>
|
||||
|
||||
{appSettings.viewAsTable ? <AppTable apps={data} /> : <AppList apps={data} />}
|
||||
|
||||
<SettingsModal opened={isSettingsOpen()} onClose={closeSettings} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { JSX, createContext, useContext } from 'solid-js';
|
||||
import { SetStoreFunction } from 'solid-js/store';
|
||||
import { createLocalStore } from '~/lib/create-local-store';
|
||||
|
||||
type Settings = { showLocations: boolean; viewAsTable: boolean };
|
||||
|
||||
type SettingsContextType = {
|
||||
appSettings: Settings;
|
||||
setAppSettings: SetStoreFunction<Settings>;
|
||||
};
|
||||
|
||||
const SettingsContext = createContext<SettingsContextType>({
|
||||
appSettings: { showLocations: false, viewAsTable: false },
|
||||
setAppSettings: () => {},
|
||||
});
|
||||
|
||||
export function SettingsProvider(props: { children: JSX.Element }) {
|
||||
const [appSettings, setAppSettings] = createLocalStore<Settings>('start-page-app-settings', {
|
||||
showLocations: false,
|
||||
viewAsTable: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={{ appSettings, setAppSettings }}>
|
||||
{props.children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return useContext(SettingsContext);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
Vendored
+21
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
export type AppDefinition = {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
tags: string;
|
||||
location?: string;
|
||||
shortcut?: string;
|
||||
};
|
||||
|
||||
export type WeatherData = {
|
||||
id: string;
|
||||
main: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
temp: number;
|
||||
feels_like: number;
|
||||
temp_min: number;
|
||||
temp_max: number;
|
||||
pressure: number;
|
||||
humidity: number;
|
||||
visibility: number;
|
||||
wind_speed: number;
|
||||
wind_deg: number;
|
||||
sunrise: number;
|
||||
sunset: number;
|
||||
};
|
||||
Reference in New Issue
Block a user