Archived
Refactor tags to categories
This commit is contained in:
+8
-16
@@ -1,20 +1,12 @@
|
|||||||
import { createResource } from 'solid-js';
|
import { createMemo, createResource } from 'solid-js';
|
||||||
import { AppDefinition } from '~/types';
|
import { CategoryDefinition } from '~/types';
|
||||||
|
|
||||||
async function getApps(): Promise<AppDefinition[]> {
|
async function getCategories(): Promise<CategoryDefinition[]> {
|
||||||
return await import('../../data/apps.yml').then((m) => m.default.apps);
|
return await import('../../data/apps.yml').then((m) => m.default.categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApps() {
|
export function createCategories() {
|
||||||
const [apps] = createResource<AppDefinition[]>(getApps);
|
const [categories] = createResource<CategoryDefinition[]>(getCategories);
|
||||||
return apps;
|
const apps = createMemo(() => categories()?.flatMap((c) => c.apps));
|
||||||
}
|
return { categories, 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,31 @@
|
|||||||
import { groupBy } from 'ramda';
|
|
||||||
import { Accessor } from 'solid-js';
|
import { Accessor } from 'solid-js';
|
||||||
import { createTagOrder } from '~/api/get-apps';
|
import { CategoryDefinition } from '~/types';
|
||||||
import { AppDefinition } from '~/types';
|
|
||||||
import { Pill } from './Pill';
|
import { Pill } from './Pill';
|
||||||
|
|
||||||
import style from './AppList.module.css';
|
import style from './AppList.module.css';
|
||||||
|
|
||||||
export function AppList({
|
export function AppList({
|
||||||
apps,
|
category,
|
||||||
tag,
|
resetCategory: resetCategory,
|
||||||
resetTag,
|
|
||||||
}: {
|
}: {
|
||||||
apps: AppDefinition[];
|
category?: Accessor<CategoryDefinition | undefined>;
|
||||||
tag?: Accessor<string | null>;
|
resetCategory?(): void;
|
||||||
resetTag?(): void;
|
|
||||||
}) {
|
}) {
|
||||||
const tagOrder = createTagOrder();
|
|
||||||
const grouped = groupBy((app) => app.tags, apps);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.AppListWrap}>
|
<div class={style.AppListWrap}>
|
||||||
{tag?.() ? (
|
<div style={{ width: '100%' }}>
|
||||||
<div style={{ width: '100%' }}>
|
<h2 class={style.Header}>
|
||||||
<h2 class={style.Header}>
|
{category?.()?.name}
|
||||||
{tag()}
|
<span class={style.Close} onClick={resetCategory}>
|
||||||
<span class={style.Close} onClick={resetTag}>
|
×
|
||||||
×
|
</span>
|
||||||
</span>
|
</h2>
|
||||||
</h2>
|
<div class={style.AppList}>
|
||||||
<div class={style.AppList}>
|
{category?.()?.apps.map((app) => (
|
||||||
{grouped[tag()!]!.map((app, i) => (
|
<Pill {...app} />
|
||||||
<Pill {...app} />
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
</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 { Tooltip } from '@hope-ui/solid';
|
||||||
import { IoSave } from 'solid-icons/io';
|
import { HiOutlineStar, HiSolidStar } from 'solid-icons/hi';
|
||||||
import { createTagOrder } from '~/api/get-apps';
|
import { Accessor } from 'solid-js';
|
||||||
import { useSettings } from '~/state/SettingsProvider';
|
import { useSettings } from '~/state/SettingsProvider';
|
||||||
import { Settings } from '../widgets/Settings';
|
import { Settings } from '../widgets/Settings';
|
||||||
|
|
||||||
import style from './Sidebar.module.css';
|
import style from './Sidebar.module.css';
|
||||||
import { HiOutlineStar, HiSolidStar } from 'solid-icons/hi';
|
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
activeTag?: string | null;
|
categories: Accessor<string[] | undefined>;
|
||||||
onSelectTag?(tag: string, mode: 'permanent' | 'temporary'): void;
|
activeCategory?: string | null;
|
||||||
|
selectCategory?(category: string, mode: 'permanent' | 'temporary'): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Sidebar(props: SidebarProps) {
|
export function Sidebar(props: SidebarProps) {
|
||||||
const tagOrder = createTagOrder();
|
|
||||||
const { onSaveBackground, onUnsaveBackground, isBackgroundSaved } = useSettings();
|
const { onSaveBackground, onUnsaveBackground, isBackgroundSaved } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={style.Sidebar}>
|
<div class={style.Sidebar}>
|
||||||
<ul class={style.Menu}>
|
<ul class={style.Menu}>
|
||||||
{tagOrder()?.map((tag) => (
|
{props.categories()?.map((category) => (
|
||||||
<li
|
<li
|
||||||
class={props.activeTag === tag ? style.Active : ''}
|
class={props.activeCategory === category ? style.Active : ''}
|
||||||
onClick={() => props.onSelectTag?.(tag, 'permanent')}
|
onClick={() => props.selectCategory?.(category, 'permanent')}
|
||||||
onMouseEnter={() => props.onSelectTag?.(tag, 'temporary')}
|
onMouseEnter={() => props.selectCategory?.(category, 'temporary')}
|
||||||
>
|
>
|
||||||
{tag}
|
{category}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
+16
-25
@@ -1,5 +1,5 @@
|
|||||||
import { createSignal, onCleanup, onMount } from 'solid-js';
|
import { createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
||||||
import { createApps } from '~/api/get-apps';
|
import { createCategories } from '~/api/get-apps';
|
||||||
import { createSearchProviders } from '~/api/get-search-providers';
|
import { createSearchProviders } from '~/api/get-search-providers';
|
||||||
import { AppList } from '~/components/apps/AppList';
|
import { AppList } from '~/components/apps/AppList';
|
||||||
import { DateView } from '~/components/widgets/Date';
|
import { DateView } from '~/components/widgets/Date';
|
||||||
@@ -8,9 +8,12 @@ import { Weather } from '~/components/widgets/Weather';
|
|||||||
import { Sidebar } from '../components/ui/Sidebar';
|
import { Sidebar } from '../components/ui/Sidebar';
|
||||||
|
|
||||||
import style from './index.module.css';
|
import style from './index.module.css';
|
||||||
|
import { createActiveCategory } from '~/state/create-active-category';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const searchProviders = createSearchProviders();
|
const searchProviders = createSearchProviders();
|
||||||
|
const { categories, apps } = createCategories();
|
||||||
|
const { activeCategory, selectCategory, resetCategory } = createActiveCategory();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
parent.addEventListener('keydown', handleKeypress);
|
parent.addEventListener('keydown', handleKeypress);
|
||||||
@@ -20,30 +23,11 @@ export default function Home() {
|
|||||||
parent.removeEventListener('keydown', handleKeypress);
|
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) {
|
function handleKeypress(e: KeyboardEvent) {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'Escape': {
|
case 'Escape': {
|
||||||
document.getElementById('search')?.blur();
|
document.getElementById('search')?.blur();
|
||||||
resetTag();
|
resetCategory();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case '/': {
|
case '/': {
|
||||||
@@ -65,11 +49,18 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main class={style.SidebarPage}>
|
<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.ContentWrapper}>
|
||||||
<div class={style.Widgets}>
|
<div class={style.Widgets}>
|
||||||
{activeTag() ? (
|
{activeCategory() ? (
|
||||||
<AppList apps={apps() ?? []} tag={activeTag} resetTag={resetTag} />
|
<AppList
|
||||||
|
category={createMemo(() => categories()?.find((c) => c.name === activeCategory()))}
|
||||||
|
resetCategory={resetCategory}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DateView />
|
<DateView />
|
||||||
|
|||||||
@@ -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';
|
import { Random } from 'unsplash-js/dist/methods/photos/types';
|
||||||
|
|
||||||
|
export type CategoryDefinition = {
|
||||||
|
name: string;
|
||||||
|
apps: AppDefinition[];
|
||||||
|
};
|
||||||
|
|
||||||
export type AppDefinition = {
|
export type AppDefinition = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user