Refactor tags to categories
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.Name {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Icon {
|
||||
max-height: 24px;
|
||||
max-width: 24px;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
22
src/state/create-active-category.ts
Normal file
22
src/state/create-active-category.ts
Normal 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 };
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user