refactor: refactor webui structure

This commit is contained in:
2025-04-24 02:23:26 +08:00
parent 68aa13e216
commit eb8f0be004
87 changed files with 407 additions and 385 deletions

View File

@@ -0,0 +1,5 @@
export class UnreachableError extends Error {
constructor(detail: string) {
super(`UnreachableError: ${detail}`);
}
}

View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -0,0 +1,5 @@
import { DOCUMENT } from './injection';
export const providePlatform = () => {
return [{ provide: DOCUMENT, useValue: document }];
};

View File

@@ -0,0 +1,7 @@
export class FeatureNotAvailablePlatformError extends Error {
constructor(feature: string, platform?: string) {
super(
`Platform error: ${feature} is not available on ${platform ?? 'current platform'}`
);
}
}

View File

@@ -0,0 +1,3 @@
import { InjectionToken } from '@outposts/injection-js';
export const DOCUMENT = new InjectionToken<Document>('DOCUMENT');

View File

@@ -0,0 +1,92 @@
import {
BookOpen,
Folders,
Settings2,
SquareTerminal,
Telescope,
} from 'lucide-react';
export const AppNavMainData = [
{
group: 'Dashboard',
items: [
{
title: 'Explore',
icon: Telescope,
children: [
{
title: 'Feed',
link: {
to: '/feed',
},
},
{
title: 'Explore',
link: {
to: '/explore',
},
},
],
},
{
title: 'Subscriptions',
link: {
to: '/subscriptions',
},
icon: Folders,
children: [
{
title: 'Manage',
link: {
to: '/subscriptions/manage',
},
},
{
title: 'Create',
link: {
to: '/subscriptions/create',
},
},
],
},
{
title: 'Playground',
icon: SquareTerminal,
link: {
to: '/playground',
},
children: [
{
title: 'GraphQL Api',
link: {
to: '/playground/graphql-api',
},
},
],
},
{
title: 'Documentation',
link: {
href: 'https://github.com/dumtruck/konobangu/wiki',
target: '_blank',
},
icon: BookOpen,
},
{
title: 'Settings',
link: {
to: '/settings',
},
icon: Settings2,
children: [
{
title: 'Downloader',
link: {
to: '/settings/downloader',
},
},
],
},
],
},
];

View File

@@ -0,0 +1,17 @@
import type { ProLinkProps } from '@/components/ui/pro-link';
import type { Injector } from '@outposts/injection-js';
import type { LucideIcon } from 'lucide-react';
export type RouterContext = {
injector: Injector;
};
export type RouteBreadcrumbItem = {
label?: string;
icon?: LucideIcon;
link?: Omit<ProLinkProps, 'aria-current' | 'current'>;
};
export interface RouteStateDataOption {
breadcrumb?: RouteBreadcrumbItem;
}

View File

@@ -0,0 +1,36 @@
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { Outlet } from '@tanstack/react-router';
export interface BuildVirtualBranchRouteOptions {
title: string;
}
export function buildVirtualBranchRouteOptions(
options: BuildVirtualBranchRouteOptions
) {
return {
beforeLoad: guardRouteIndexAsNotFound,
staticData: {
breadcrumb: {
label: options.title,
link: undefined,
},
} satisfies RouteStateDataOption,
component: Outlet,
};
}
export interface BuildLeafRouteStaticDataOptions {
title: string;
}
export function buildLeafRouteStaticData(
options: BuildLeafRouteStaticDataOptions
): RouteStateDataOption {
return {
breadcrumb: {
label: options.title,
},
};
}

View File

@@ -0,0 +1,17 @@
import {
LocalStorageService,
SessionStorageService,
} from './web-storage.service';
export function provideStorages() {
return [
{
provide: LocalStorageService,
useClass: LocalStorageService,
},
{
provide: SessionStorageService,
useClass: SessionStorageService,
},
];
}

View File

@@ -0,0 +1,43 @@
import { FeatureNotAvailablePlatformError } from '@/infra/platform/errors';
import { DOCUMENT } from '@/infra/platform/injection';
import { Injectable, inject } from '@outposts/injection-js';
@Injectable()
export class LocalStorageService {
document = inject(DOCUMENT);
storage = this.document.defaultView?.localStorage;
setItem(key: string, value: string) {
if (!this.storage) {
throw new FeatureNotAvailablePlatformError('local-storage');
}
this.storage.setItem(key, value);
}
getItem(key: string) {
if (!this.storage) {
throw new FeatureNotAvailablePlatformError('local-storage');
}
return this.storage.getItem(key);
}
}
@Injectable()
export class SessionStorageService {
document = inject(DOCUMENT);
storage = this.document.defaultView?.sessionStorage;
setItem(key: string, value: string) {
if (!this.storage) {
throw new FeatureNotAvailablePlatformError('session-storage');
}
this.storage.setItem(key, value);
}
getItem(key: string) {
if (!this.storage) {
throw new FeatureNotAvailablePlatformError('session-storage');
}
return this.storage.getItem(key);
}
}

View File

@@ -0,0 +1,10 @@
import { ThemeService } from './theme.service';
export function provideStyles() {
return [
{
provide: ThemeService,
useClass: ThemeService,
},
];
}

View File

@@ -0,0 +1,41 @@
import { DOCUMENT } from '@/infra/platform/injection';
import { LocalStorageService } from '@/infra/storage/web-storage.service';
import { Injectable, inject } from '@outposts/injection-js';
export type PreferColorSchemaType = 'dark' | 'light' | 'system';
export type PreferColorSchemaClass = 'dark' | 'light';
@Injectable()
export class ThemeService {
document = inject(DOCUMENT);
localStorage = inject(LocalStorageService);
get systemColorSchema(): PreferColorSchemaClass {
return this.document.defaultView?.matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
}
private getColorSchemaByType(
themeType: PreferColorSchemaType
): PreferColorSchemaClass {
this.document.documentElement.classList.remove('dark', 'light');
if (themeType === 'dark' || themeType === 'light') {
return themeType;
}
return this.systemColorSchema;
}
get colorSchema() {
const theme = this.localStorage.getItem('prefers-color-scheme');
return this.getColorSchemaByType(theme as PreferColorSchemaType);
}
set colorSchema(themeType: PreferColorSchemaType) {
this.localStorage.setItem('prefers-color-scheme', themeType);
const themeClass = this.getColorSchemaByType(themeType);
this.document.documentElement.classList.remove('dark', 'light');
this.document.documentElement.classList.add(themeClass);
}
}

View File

@@ -0,0 +1,7 @@
import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}