feat(frontend): お知らせページ

close #110
This commit is contained in:
Xeltica 2023-04-30 02:20:09 +09:00
parent 7a361ebaa0
commit ffdc32ba47
11 changed files with 167 additions and 50 deletions

View File

@ -19,7 +19,9 @@
"@radix-ui/react-radio-group": "^1.1.2",
"@stitches/react": "^1.2.8",
"@tabler/icons-webfont": "^2.17.0",
"@tanstack/react-query": "^4.29.5",
"@trpc/client": "10.20.0",
"@trpc/react-query": "^10.23.0",
"@trpc/server": "10.20.0",
"dayjs": "^1.11.7",
"deepmerge": "^4.3.1",

View File

@ -1,10 +1,8 @@
import React, { lazy } from 'react';
import { Route, Routes } from 'react-router-dom';
import { SuspenseView } from './components/primitives/SuspenseView';
import { HeaderBar } from '@/components/HeaderBar.js';
import { SuspenseView } from '@/components/primitives/SuspenseView';
import { useToolsGlobalEffects } from '@/global-effects';
import { token } from '@/misc/token';
@ -13,6 +11,8 @@ const IndexWelcome = lazy(() => import('@/pages/index.welcome'));
const Settings = lazy(() => import('@/pages/settings'));
const Appearance = lazy(() => import('@/pages/settings/appearance'));
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 = () => {
useToolsGlobalEffects();
@ -28,6 +28,7 @@ export const App : React.FC = () => {
<Route path="account" element={<Account />}/>
<Route path="*" element={<p>Not Found</p>}/>
</Route>
<Route path="/announcements/:id" element={<AnnouncementsPage />}/>
<Route path="*" element={<NotFound />}/>
</Routes>
</SuspenseView>

View File

@ -5,6 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
import { App } from '@/App';
import { BackendError } from '@/components/domains/backend-error/BackendError.js';
import { TRPCProvider } from '@/libs/trpc';
import 'vite/modulepreload-polyfill';
import 'ress';
@ -20,8 +21,10 @@ const error = (window as any).__misshaialert?.error;
if (el != null) {
createRoot(el).render(error ? (<BackendError error={error} />) : (
<BrowserRouter>
<App />
</BrowserRouter>
<TRPCProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</TRPCProvider>
));
}

View 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>
);
};

View 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" />&nbsp;
{dayjs(announcement.createdAt).locale(language.split('_')[0]).fromNow()}
</Text>
<Section>
<ReactMarkdown>{announcement.body}</ReactMarkdown>
</Section>
</article>
</PageRoot>
);
};
export default AnnouncementsPage;

View File

@ -58,22 +58,13 @@ const SettingsPage: React.FC = () => {
href: '/settings/appearance',
iconClassName: 'ti ti-palette',
label: t('appearance') ?? '',
}, {
type: 'link',
href: '/settings/misshai',
iconClassName: 'ti ti-antenna',
label: t('missHaiAlert') ?? '',
}, {
type: 'separator',
}, {
type: 'button',
iconClassName: 'ti ti-logout',
label: t('logout') ?? '',
} , {
type: 'button',
iconClassName: 'ti ti-trash',
label: t('deleteAccount') ?? '',
} ] as MenuItemWithoutNesting[], [t]);
}] as MenuItemWithoutNesting[], [t]);
const location = useLocation();
const navigate = useNavigate();

View File

@ -1,4 +1,4 @@
import { trpcJotai } from '@/store/api';
import { trpcJotai } from '@/libs/trpc';
export const accountAtom = trpcJotai.account.getMyself.atomWithQuery();

View File

@ -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();

View File

@ -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 ],
});

View File

@ -1,3 +1,3 @@
import { trpcJotai } from '.';
import { trpcJotai } from '@/libs/trpc';
export const metaAtom = trpcJotai.meta.get.atomWithQuery();

View File

@ -176,9 +176,15 @@ importers:
'@tabler/icons-webfont':
specifier: ^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':
specifier: 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':
specifier: 10.20.0
version: 10.20.0
@ -3706,6 +3712,28 @@ packages:
resolution: {integrity: sha512-UeJaylOGNRhQKyDlgZfrQ3UPSGlfVQuXcmCsTYeXioKKepibW6VZ3H36Lo1jvBTBkQD2e9m+k2NxwkztOTXwrA==}
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:
resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==}
engines: {node: '>=12'}
@ -3742,6 +3770,22 @@ packages:
'@trpc/server': 10.20.0
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:
resolution: {integrity: sha512-pbrdgw2mO8lWygyZjGLbOq/EoWFWEJvLaxp8JmGsivMIh/IZR5mksPJdywAGQ4TBrbcqssKhuoJ/8OE9mhaveg==}
dev: false
@ -12617,6 +12661,14 @@ packages:
tslib: 2.5.0
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:
resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
engines: {node: '>=0.10.0'}