refactor: refactor webui structure
This commit is contained in:
5
apps/webui/src/infra/errors/common.ts
Normal file
5
apps/webui/src/infra/errors/common.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class UnreachableError extends Error {
|
||||
constructor(detail: string) {
|
||||
super(`UnreachableError: ${detail}`);
|
||||
}
|
||||
}
|
||||
19
apps/webui/src/infra/hooks/use-mobile.ts
Normal file
19
apps/webui/src/infra/hooks/use-mobile.ts
Normal 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
|
||||
}
|
||||
5
apps/webui/src/infra/platform/context.ts
Normal file
5
apps/webui/src/infra/platform/context.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DOCUMENT } from './injection';
|
||||
|
||||
export const providePlatform = () => {
|
||||
return [{ provide: DOCUMENT, useValue: document }];
|
||||
};
|
||||
7
apps/webui/src/infra/platform/errors.ts
Normal file
7
apps/webui/src/infra/platform/errors.ts
Normal 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'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
3
apps/webui/src/infra/platform/injection.ts
Normal file
3
apps/webui/src/infra/platform/injection.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { InjectionToken } from '@outposts/injection-js';
|
||||
|
||||
export const DOCUMENT = new InjectionToken<Document>('DOCUMENT');
|
||||
92
apps/webui/src/infra/routes/nav.ts
Normal file
92
apps/webui/src/infra/routes/nav.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
17
apps/webui/src/infra/routes/traits.ts
Normal file
17
apps/webui/src/infra/routes/traits.ts
Normal 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;
|
||||
}
|
||||
36
apps/webui/src/infra/routes/utils.ts
Normal file
36
apps/webui/src/infra/routes/utils.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
17
apps/webui/src/infra/storage/context.ts
Normal file
17
apps/webui/src/infra/storage/context.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
LocalStorageService,
|
||||
SessionStorageService,
|
||||
} from './web-storage.service';
|
||||
|
||||
export function provideStorages() {
|
||||
return [
|
||||
{
|
||||
provide: LocalStorageService,
|
||||
useClass: LocalStorageService,
|
||||
},
|
||||
{
|
||||
provide: SessionStorageService,
|
||||
useClass: SessionStorageService,
|
||||
},
|
||||
];
|
||||
}
|
||||
43
apps/webui/src/infra/storage/web-storage.service.ts
Normal file
43
apps/webui/src/infra/storage/web-storage.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
10
apps/webui/src/infra/styles/context.ts
Normal file
10
apps/webui/src/infra/styles/context.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ThemeService } from './theme.service';
|
||||
|
||||
export function provideStyles() {
|
||||
return [
|
||||
{
|
||||
provide: ThemeService,
|
||||
useClass: ThemeService,
|
||||
},
|
||||
];
|
||||
}
|
||||
41
apps/webui/src/infra/styles/theme.service.ts
Normal file
41
apps/webui/src/infra/styles/theme.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
7
apps/webui/src/infra/styles/utils.ts
Normal file
7
apps/webui/src/infra/styles/utils.ts
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user