parent
7a361ebaa0
commit
ffdc32ba47
@ -19,7 +19,9 @@
|
|||||||
"@radix-ui/react-radio-group": "^1.1.2",
|
"@radix-ui/react-radio-group": "^1.1.2",
|
||||||
"@stitches/react": "^1.2.8",
|
"@stitches/react": "^1.2.8",
|
||||||
"@tabler/icons-webfont": "^2.17.0",
|
"@tabler/icons-webfont": "^2.17.0",
|
||||||
|
"@tanstack/react-query": "^4.29.5",
|
||||||
"@trpc/client": "10.20.0",
|
"@trpc/client": "10.20.0",
|
||||||
|
"@trpc/react-query": "^10.23.0",
|
||||||
"@trpc/server": "10.20.0",
|
"@trpc/server": "10.20.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React, { lazy } from 'react';
|
import React, { lazy } from 'react';
|
||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
|
||||||
import { SuspenseView } from './components/primitives/SuspenseView';
|
|
||||||
|
|
||||||
import { HeaderBar } from '@/components/HeaderBar.js';
|
import { HeaderBar } from '@/components/HeaderBar.js';
|
||||||
|
import { SuspenseView } from '@/components/primitives/SuspenseView';
|
||||||
import { useToolsGlobalEffects } from '@/global-effects';
|
import { useToolsGlobalEffects } from '@/global-effects';
|
||||||
import { token } from '@/misc/token';
|
import { token } from '@/misc/token';
|
||||||
|
|
||||||
@ -13,6 +11,8 @@ const IndexWelcome = lazy(() => import('@/pages/index.welcome'));
|
|||||||
const Settings = lazy(() => import('@/pages/settings'));
|
const Settings = lazy(() => import('@/pages/settings'));
|
||||||
const Appearance = lazy(() => import('@/pages/settings/appearance'));
|
const Appearance = lazy(() => import('@/pages/settings/appearance'));
|
||||||
const Account = lazy(() => import('@/pages/settings/account'));
|
const Account = lazy(() => import('@/pages/settings/account'));
|
||||||
|
const AnnouncementsPage = lazy(() => import('@/pages/announcements'));
|
||||||
|
const NotFound = lazy(() => import('@/pages/not-found'));
|
||||||
|
|
||||||
export const App : React.FC = () => {
|
export const App : React.FC = () => {
|
||||||
useToolsGlobalEffects();
|
useToolsGlobalEffects();
|
||||||
@ -28,6 +28,7 @@ export const App : React.FC = () => {
|
|||||||
<Route path="account" element={<Account />}/>
|
<Route path="account" element={<Account />}/>
|
||||||
<Route path="*" element={<p>Not Found</p>}/>
|
<Route path="*" element={<p>Not Found</p>}/>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/announcements/:id" element={<AnnouncementsPage />}/>
|
||||||
<Route path="*" element={<NotFound />}/>
|
<Route path="*" element={<NotFound />}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</SuspenseView>
|
</SuspenseView>
|
||||||
|
@ -5,6 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
|
|||||||
|
|
||||||
import { App } from '@/App';
|
import { App } from '@/App';
|
||||||
import { BackendError } from '@/components/domains/backend-error/BackendError.js';
|
import { BackendError } from '@/components/domains/backend-error/BackendError.js';
|
||||||
|
import { TRPCProvider } from '@/libs/trpc';
|
||||||
|
|
||||||
import 'vite/modulepreload-polyfill';
|
import 'vite/modulepreload-polyfill';
|
||||||
import 'ress';
|
import 'ress';
|
||||||
@ -20,8 +21,10 @@ const error = (window as any).__misshaialert?.error;
|
|||||||
|
|
||||||
if (el != null) {
|
if (el != null) {
|
||||||
createRoot(el).render(error ? (<BackendError error={error} />) : (
|
createRoot(el).render(error ? (<BackendError error={error} />) : (
|
||||||
<BrowserRouter>
|
<TRPCProvider>
|
||||||
<App />
|
<BrowserRouter>
|
||||||
</BrowserRouter>
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</TRPCProvider>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
45
packages/frontend/src/libs/trpc.tsx
Normal file
45
packages/frontend/src/libs/trpc.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { httpLink } from '@trpc/client';
|
||||||
|
import { createTRPCReact } from '@trpc/react-query';
|
||||||
|
import { createTRPCJotai } from 'jotai-trpc';
|
||||||
|
import React, { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import type { AppRouter } from 'tools-backend';
|
||||||
|
|
||||||
|
import { LOCALSTORAGE_KEY_TOKEN } from '@/const';
|
||||||
|
|
||||||
|
const link = httpLink({
|
||||||
|
url: `${location.origin}/api`,
|
||||||
|
headers() {
|
||||||
|
const token = localStorage[LOCALSTORAGE_KEY_TOKEN];
|
||||||
|
if (!token) return {};
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
links: [ link ],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trpcJotai = createTRPCJotai<AppRouter>({
|
||||||
|
links: [ link ],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trpc = createTRPCReact<AppRouter, unknown, 'ExperimentalSuspense'>();
|
||||||
|
|
||||||
|
const providerInit = {
|
||||||
|
client: trpc.createClient(init),
|
||||||
|
queryClient: new QueryClient(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRPCProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<trpc.Provider {...providerInit}>
|
||||||
|
<QueryClientProvider client={providerInit.queryClient}>
|
||||||
|
{children}
|
||||||
|
</QueryClientProvider>
|
||||||
|
</trpc.Provider>
|
||||||
|
);
|
||||||
|
};
|
55
packages/frontend/src/pages/announcements.tsx
Normal file
55
packages/frontend/src/pages/announcements.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PageRoot } from '@/components/PageRoot';
|
||||||
|
import { Text } from '@/components/primitives/Text';
|
||||||
|
import { styled } from '@/libs/stitches';
|
||||||
|
import { trpc } from '@/libs/trpc';
|
||||||
|
import { languageAtom } from '@/store/client-settings';
|
||||||
|
|
||||||
|
const AnnouncementsPage: React.FC = () => {
|
||||||
|
const { id } = useParams<{id: string}>();
|
||||||
|
|
||||||
|
const [announcement] = trpc.announcements.get.useSuspenseQuery(Number(id));
|
||||||
|
|
||||||
|
const language = useAtomValue(languageAtom);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const Section = styled('section', {
|
||||||
|
padding: '$m',
|
||||||
|
'h1, h2, h3': {
|
||||||
|
marginTop: '$l',
|
||||||
|
marginBottom: '$xs',
|
||||||
|
},
|
||||||
|
'p, ul': {
|
||||||
|
marginBottom: '$m',
|
||||||
|
},
|
||||||
|
li: {
|
||||||
|
marginLeft: '$l',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageRoot title={t('announcements') ?? ''}>
|
||||||
|
<article>
|
||||||
|
<h2>
|
||||||
|
{announcement.title}{' '}
|
||||||
|
</h2>
|
||||||
|
<Text as="aside" color="muted" fontSize="m" css={{ marginLeft: '$l' }}>
|
||||||
|
<i className="ti ti-clock" />
|
||||||
|
{dayjs(announcement.createdAt).locale(language.split('_')[0]).fromNow()}
|
||||||
|
</Text>
|
||||||
|
<Section>
|
||||||
|
<ReactMarkdown>{announcement.body}</ReactMarkdown>
|
||||||
|
</Section>
|
||||||
|
</article>
|
||||||
|
</PageRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnnouncementsPage;
|
@ -58,22 +58,13 @@ const SettingsPage: React.FC = () => {
|
|||||||
href: '/settings/appearance',
|
href: '/settings/appearance',
|
||||||
iconClassName: 'ti ti-palette',
|
iconClassName: 'ti ti-palette',
|
||||||
label: t('appearance') ?? '',
|
label: t('appearance') ?? '',
|
||||||
}, {
|
|
||||||
type: 'link',
|
|
||||||
href: '/settings/misshai',
|
|
||||||
iconClassName: 'ti ti-antenna',
|
|
||||||
label: t('missHaiAlert') ?? '',
|
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
}, {
|
}, {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
iconClassName: 'ti ti-logout',
|
iconClassName: 'ti ti-logout',
|
||||||
label: t('logout') ?? '',
|
label: t('logout') ?? '',
|
||||||
} , {
|
}] as MenuItemWithoutNesting[], [t]);
|
||||||
type: 'button',
|
|
||||||
iconClassName: 'ti ti-trash',
|
|
||||||
label: t('deleteAccount') ?? '',
|
|
||||||
} ] as MenuItemWithoutNesting[], [t]);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { trpcJotai } from '@/store/api';
|
import { trpcJotai } from '@/libs/trpc';
|
||||||
|
|
||||||
export const accountAtom = trpcJotai.account.getMyself.atomWithQuery();
|
export const accountAtom = trpcJotai.account.getMyself.atomWithQuery();
|
||||||
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
import { atom } from 'jotai';
|
|
||||||
|
|
||||||
import { trpcJotai } from '@/store/api';
|
|
||||||
|
|
||||||
export const announcementListAtom = trpcJotai.announcements.getAll.atomWithQuery();
|
|
||||||
|
|
||||||
export const currentAnnouncementIdAtom = atom(0);
|
|
||||||
|
|
||||||
export const announcementAtom = trpcJotai.announcements.get.atomWithQuery((get) => get(currentAnnouncementIdAtom));
|
|
||||||
|
|
||||||
export const likeAtom = trpcJotai.announcements.like.atomWithMutation();
|
|
@ -1,21 +0,0 @@
|
|||||||
import { httpLink } from '@trpc/client';
|
|
||||||
import { createTRPCJotai } from 'jotai-trpc';
|
|
||||||
|
|
||||||
import type { AppRouter } from 'tools-backend';
|
|
||||||
|
|
||||||
import { LOCALSTORAGE_KEY_TOKEN } from '@/const';
|
|
||||||
|
|
||||||
const link = httpLink({
|
|
||||||
url: `${location.origin}/api`,
|
|
||||||
headers() {
|
|
||||||
const token = localStorage[LOCALSTORAGE_KEY_TOKEN];
|
|
||||||
if (!token) return {};
|
|
||||||
return {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const trpcJotai = createTRPCJotai<AppRouter>({
|
|
||||||
links: [ link ],
|
|
||||||
});
|
|
@ -1,3 +1,3 @@
|
|||||||
import { trpcJotai } from '.';
|
import { trpcJotai } from '@/libs/trpc';
|
||||||
|
|
||||||
export const metaAtom = trpcJotai.meta.get.atomWithQuery();
|
export const metaAtom = trpcJotai.meta.get.atomWithQuery();
|
||||||
|
@ -176,9 +176,15 @@ importers:
|
|||||||
'@tabler/icons-webfont':
|
'@tabler/icons-webfont':
|
||||||
specifier: ^2.17.0
|
specifier: ^2.17.0
|
||||||
version: 2.17.0
|
version: 2.17.0
|
||||||
|
'@tanstack/react-query':
|
||||||
|
specifier: ^4.29.5
|
||||||
|
version: 4.29.5(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@trpc/client':
|
'@trpc/client':
|
||||||
specifier: 10.20.0
|
specifier: 10.20.0
|
||||||
version: 10.20.0(@trpc/server@10.20.0)
|
version: 10.20.0(@trpc/server@10.20.0)
|
||||||
|
'@trpc/react-query':
|
||||||
|
specifier: ^10.23.0
|
||||||
|
version: 10.23.0(@tanstack/react-query@4.29.5)(@trpc/client@10.20.0)(@trpc/server@10.20.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@trpc/server':
|
'@trpc/server':
|
||||||
specifier: 10.20.0
|
specifier: 10.20.0
|
||||||
version: 10.20.0
|
version: 10.20.0
|
||||||
@ -3706,6 +3712,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-UeJaylOGNRhQKyDlgZfrQ3UPSGlfVQuXcmCsTYeXioKKepibW6VZ3H36Lo1jvBTBkQD2e9m+k2NxwkztOTXwrA==}
|
resolution: {integrity: sha512-UeJaylOGNRhQKyDlgZfrQ3UPSGlfVQuXcmCsTYeXioKKepibW6VZ3H36Lo1jvBTBkQD2e9m+k2NxwkztOTXwrA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/query-core@4.29.5:
|
||||||
|
resolution: {integrity: sha512-xXIiyQ/4r9KfaJ3k6kejqcaqFXXBTzN2aOJ5H1J6aTJE9hl/nbgAdfF6oiIu0CD5xowejJEJ6bBg8TO7BN4NuQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@tanstack/react-query@4.29.5(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-F87cibC3s3eG0Q90g2O+hqntpCrudKFnR8P24qkH9uccEhXErnJxBC/AAI4cJRV2bfMO8IeGZQYf3WyYgmSg0w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-native: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-native:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/query-core': 4.29.5
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@testing-library/dom@8.20.0:
|
/@testing-library/dom@8.20.0:
|
||||||
resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==}
|
resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -3742,6 +3770,22 @@ packages:
|
|||||||
'@trpc/server': 10.20.0
|
'@trpc/server': 10.20.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@trpc/react-query@10.23.0(@tanstack/react-query@4.29.5)(@trpc/client@10.20.0)(@trpc/server@10.20.0)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-50vfR3HtOEcjCQMuHSGTFOG6W9WunvcI8msTp84UNAqZ9dz4YdjgBVNn2wxqB5LAQ6AGlcKuzJWjEj3yO56tvw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tanstack/react-query': ^4.18.0
|
||||||
|
'@trpc/client': 10.23.0
|
||||||
|
'@trpc/server': 10.23.0
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/react-query': 4.29.5(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@trpc/client': 10.20.0(@trpc/server@10.20.0)
|
||||||
|
'@trpc/server': 10.20.0
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@trpc/server@10.20.0:
|
/@trpc/server@10.20.0:
|
||||||
resolution: {integrity: sha512-pbrdgw2mO8lWygyZjGLbOq/EoWFWEJvLaxp8JmGsivMIh/IZR5mksPJdywAGQ4TBrbcqssKhuoJ/8OE9mhaveg==}
|
resolution: {integrity: sha512-pbrdgw2mO8lWygyZjGLbOq/EoWFWEJvLaxp8JmGsivMIh/IZR5mksPJdywAGQ4TBrbcqssKhuoJ/8OE9mhaveg==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -12617,6 +12661,14 @@ packages:
|
|||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/use@3.1.1:
|
/use@3.1.1:
|
||||||
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
|
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
Loading…
Reference in New Issue
Block a user