Refactor tags to categories

This commit is contained in:
Georgi Gardev
2023-11-19 09:45:33 +02:00
parent 4d56c1e169
commit 99e98aeb41
8 changed files with 78 additions and 163 deletions

View File

@@ -1,20 +1,12 @@
import { createResource } from 'solid-js';
import { AppDefinition } from '~/types';
import { createMemo, createResource } from 'solid-js';
import { CategoryDefinition } from '~/types';
async function getApps(): Promise<AppDefinition[]> {
return await import('../../data/apps.yml').then((m) => m.default.apps);
async function getCategories(): Promise<CategoryDefinition[]> {
return await import('../../data/apps.yml').then((m) => m.default.categories);
}
export function createApps() {
const [apps] = createResource<AppDefinition[]>(getApps);
return apps;
}
export async function getTagOrder(): Promise<string[]> {
return await import('../../data/tags.yml').then((m) => m.default.tags);
}
export function createTagOrder() {
const [tagOrder] = createResource<string[]>(getTagOrder);
return tagOrder;
export function createCategories() {
const [categories] = createResource<CategoryDefinition[]>(getCategories);
const apps = createMemo(() => categories()?.flatMap((c) => c.apps));
return { categories, apps };
}

View File

@@ -1,51 +1,31 @@
import { groupBy } from 'ramda';
import { Accessor } from 'solid-js';
import { createTagOrder } from '~/api/get-apps';
import { AppDefinition } from '~/types';
import { CategoryDefinition } from '~/types';
import { Pill } from './Pill';
import style from './AppList.module.css';
export function AppList({
apps,
tag,
resetTag,
category,
resetCategory: resetCategory,
}: {
apps: AppDefinition[];
tag?: Accessor<string | null>;
resetTag?(): void;
category?: Accessor<CategoryDefinition | undefined>;
resetCategory?(): void;
}) {
const tagOrder = createTagOrder();
const grouped = groupBy((app) => app.tags, apps);
return (
<div class={style.AppListWrap}>
{tag?.() ? (
<div style={{ width: '100%' }}>
<h2 class={style.Header}>
{tag()}
<span class={style.Close} onClick={resetTag}>
×
</span>
</h2>
<div class={style.AppList}>
{grouped[tag()!]!.map((app, i) => (
<Pill {...app} />
))}
</div>
<div style={{ width: '100%' }}>
<h2 class={style.Header}>
{category?.()?.name}
<span class={style.Close} onClick={resetCategory}>
×
</span>
</h2>
<div class={style.AppList}>
{category?.()?.apps.map((app) => (
<Pill {...app} />
))}
</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>
</div>
);
}

View File

@@ -1,10 +0,0 @@
.Name {
display: flex;
gap: 1rem;
align-items: center;
}
.Icon {
max-height: 24px;
max-width: 24px;
}

View File

@@ -1,64 +0,0 @@
import { Tag, TagProps } from '@hope-ui/solid';
import { getAppIcon } from '~/api/get-app-icon';
import { createTagOrder } from '~/api/get-apps';
import { useSettings } from '~/state/SettingsProvider';
import { AppDefinition } from '~/types';
import style from './AppTable.module.css';
export function AppTable({ apps }: { apps: AppDefinition[] }) {
const { appSettings } = useSettings();
const tagOrder = createTagOrder();
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) ?? 0) - (tagOrder()?.indexOf(tagB.tags) ?? 0)
)
.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>
);
}

View File

@@ -1,31 +1,30 @@
import { Tooltip } from '@hope-ui/solid';
import { IoSave } from 'solid-icons/io';
import { createTagOrder } from '~/api/get-apps';
import { HiOutlineStar, HiSolidStar } from 'solid-icons/hi';
import { Accessor } from 'solid-js';
import { useSettings } from '~/state/SettingsProvider';
import { Settings } from '../widgets/Settings';
import style from './Sidebar.module.css';
import { HiOutlineStar, HiSolidStar } from 'solid-icons/hi';
type SidebarProps = {
activeTag?: string | null;
onSelectTag?(tag: string, mode: 'permanent' | 'temporary'): void;
categories: Accessor<string[] | undefined>;
activeCategory?: string | null;
selectCategory?(category: string, mode: 'permanent' | 'temporary'): void;
};
export function Sidebar(props: SidebarProps) {
const tagOrder = createTagOrder();
const { onSaveBackground, onUnsaveBackground, isBackgroundSaved } = useSettings();
return (
<div class={style.Sidebar}>
<ul class={style.Menu}>
{tagOrder()?.map((tag) => (
{props.categories()?.map((category) => (
<li
class={props.activeTag === tag ? style.Active : ''}
onClick={() => props.onSelectTag?.(tag, 'permanent')}
onMouseEnter={() => props.onSelectTag?.(tag, 'temporary')}
class={props.activeCategory === category ? style.Active : ''}
onClick={() => props.selectCategory?.(category, 'permanent')}
onMouseEnter={() => props.selectCategory?.(category, 'temporary')}
>
{tag}
{category}
</li>
))}
</ul>

View File

@@ -1,5 +1,5 @@
import { createSignal, onCleanup, onMount } from 'solid-js';
import { createApps } from '~/api/get-apps';
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js';
import { createCategories } from '~/api/get-apps';
import { createSearchProviders } from '~/api/get-search-providers';
import { AppList } from '~/components/apps/AppList';
import { DateView } from '~/components/widgets/Date';
@@ -8,9 +8,12 @@ import { Weather } from '~/components/widgets/Weather';
import { Sidebar } from '../components/ui/Sidebar';
import style from './index.module.css';
import { createActiveCategory } from '~/state/create-active-category';
export default function Home() {
const searchProviders = createSearchProviders();
const { categories, apps } = createCategories();
const { activeCategory, selectCategory, resetCategory } = createActiveCategory();
onMount(() => {
parent.addEventListener('keydown', handleKeypress);
@@ -20,30 +23,11 @@ export default function Home() {
parent.removeEventListener('keydown', handleKeypress);
});
const apps = createApps();
const [activeTag, setActiveTag] = createSignal<string | null>(null);
const [timeout, storeTimeout] = createSignal<NodeJS.Timeout | null>(null);
const switchToTag = (tag: string, mode: 'permanent' | 'temporary') => {
setActiveTag(tag);
clearTimeout(timeout() as NodeJS.Timeout);
if (mode === 'temporary') {
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();
resetCategory();
break;
}
case '/': {
@@ -65,11 +49,18 @@ export default function Home() {
return (
<main class={style.SidebarPage}>
<Sidebar onSelectTag={switchToTag} activeTag={activeTag()} />
<Sidebar
categories={createMemo(() => categories()?.flatMap((c) => c.name))}
selectCategory={selectCategory}
activeCategory={activeCategory()}
/>
<div class={style.ContentWrapper}>
<div class={style.Widgets}>
{activeTag() ? (
<AppList apps={apps() ?? []} tag={activeTag} resetTag={resetTag} />
{activeCategory() ? (
<AppList
category={createMemo(() => categories()?.find((c) => c.name === activeCategory()))}
resetCategory={resetCategory}
/>
) : (
<>
<DateView />

View File

@@ -0,0 +1,22 @@
import { createSignal } from 'solid-js';
export function createActiveCategory() {
const [activeCategory, setActiveCategory] = createSignal<string | null>(null);
const [timeout, storeTimeout] = createSignal<NodeJS.Timeout | null>(null);
const selectCategory = (category: string, mode: 'permanent' | 'temporary') => {
setActiveCategory(category);
clearTimeout(timeout() as NodeJS.Timeout);
if (mode === 'temporary') {
storeTimeout(setTimeout(() => setActiveCategory(null), 3000));
}
};
const resetCategory = () => {
setActiveCategory(null);
clearTimeout(timeout() as NodeJS.Timeout);
};
return { activeCategory, selectCategory, resetCategory };
}

View File

@@ -1,5 +1,10 @@
import { Random } from 'unsplash-js/dist/methods/photos/types';
export type CategoryDefinition = {
name: string;
apps: AppDefinition[];
};
export type AppDefinition = {
name: string;
url: string;