Merge branch 'feature/renewal' into l10n_master
This commit is contained in:
commit
c7fe03e263
37 changed files with 834 additions and 440 deletions
|
@ -27,6 +27,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@reduxjs/toolkit": "^1.6.1",
|
"@reduxjs/toolkit": "^1.6.1",
|
||||||
|
"@types/deepmerge": "^2.2.0",
|
||||||
"@types/insert-text-at-cursor": "^0.3.0",
|
"@types/insert-text-at-cursor": "^0.3.0",
|
||||||
"@types/koa-bodyparser": "^4.3.0",
|
"@types/koa-bodyparser": "^4.3.0",
|
||||||
"@types/koa-multer": "^1.0.0",
|
"@types/koa-multer": "^1.0.0",
|
||||||
|
@ -38,6 +39,7 @@
|
||||||
"@types/react": "^17.0.19",
|
"@types/react": "^17.0.19",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.1.8",
|
"@types/react-router-dom": "^5.1.8",
|
||||||
|
"@types/react-twemoji": "^0.4.0",
|
||||||
"@types/styled-components": "^5.1.13",
|
"@types/styled-components": "^5.1.13",
|
||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.0.0",
|
||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
|
@ -45,6 +47,7 @@
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"css-loader": "^6.2.0",
|
"css-loader": "^6.2.0",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
"delay": "^4.4.0",
|
"delay": "^4.4.0",
|
||||||
"fibers": "^5.0.0",
|
"fibers": "^5.0.0",
|
||||||
"i18next": "^20.6.1",
|
"i18next": "^20.6.1",
|
||||||
|
@ -73,6 +76,7 @@
|
||||||
"react-modal-hook": "^3.0.0",
|
"react-modal-hook": "^3.0.0",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.4",
|
||||||
"react-router-dom": "^5.2.1",
|
"react-router-dom": "^5.2.1",
|
||||||
|
"react-twemoji": "^0.5.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rndstr": "^1.0.0",
|
"rndstr": "^1.0.0",
|
||||||
"routing-controllers": "^0.9.0",
|
"routing-controllers": "^0.9.0",
|
||||||
|
|
|
@ -15,7 +15,7 @@ html
|
||||||
meta(name='twitter:site' content='@Xeltica')
|
meta(name='twitter:site' content='@Xeltica')
|
||||||
meta(name='twitter:creator' content='@Xeltica')
|
meta(name='twitter:creator' content='@Xeltica')
|
||||||
link(rel="stylesheet" href="https://koruri.chillout.chat/koruri.css")
|
link(rel="stylesheet" href="https://koruri.chillout.chat/koruri.css")
|
||||||
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css")
|
script(src='https://kit.fontawesome.com/c7ab6eba70.js' crossorigin='anonymous')
|
||||||
style.
|
style.
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,91 +1,64 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom';
|
import { BrowserRouter, useLocation } from 'react-router-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider, useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from 'i18next';
|
|
||||||
import { initReactI18next } from 'react-i18next';
|
|
||||||
|
|
||||||
import { IndexPage } from './pages';
|
|
||||||
import { RankingPage } from './pages/ranking';
|
|
||||||
import { Header } from './components/Header';
|
|
||||||
import { TermPage } from './pages/term';
|
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
import { ModalComponent } from './Modal';
|
import { ModalComponent } from './Modal';
|
||||||
import { useTheme } from './misc/theme';
|
import { useTheme } from './misc/theme';
|
||||||
import { getBrowserLanguage, resources } from './langs';
|
import { BREAKPOINT_SM, XELTICA_STUDIO_URL } from './const';
|
||||||
import { LOCALSTORAGE_KEY_LANG, XELTICA_STUDIO_URL } from './const';
|
|
||||||
import { useGetSessionQuery } from './services/session';
|
import { useGetSessionQuery } from './services/session';
|
||||||
import { AnnouncementPage } from './pages/announcement';
|
import { Router } from './Router';
|
||||||
|
import { setMobile } from './store/slices/screen';
|
||||||
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
|
import { GeneralLayout } from './GeneralLayout';
|
||||||
import './style.scss';
|
|
||||||
|
|
||||||
document.body.classList.add('dark');
|
|
||||||
|
|
||||||
if (!localStorage[LOCALSTORAGE_KEY_LANG]) {
|
|
||||||
localStorage[LOCALSTORAGE_KEY_LANG] = getBrowserLanguage();
|
|
||||||
}
|
|
||||||
|
|
||||||
i18n
|
|
||||||
.use(initReactI18next)
|
|
||||||
.init({
|
|
||||||
resources,
|
|
||||||
lng: localStorage[LOCALSTORAGE_KEY_LANG],
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false // react already safes from xss
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const AppInner : React.VFC = () => {
|
const AppInner : React.VFC = () => {
|
||||||
const { data: session } = useGetSessionQuery(undefined);
|
const { data: session } = useGetSessionQuery(undefined);
|
||||||
const $location = useLocation();
|
const $location = useLocation();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useTheme();
|
useTheme();
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const error = (window as any).__misshaialert?.error;
|
const error = (window as any).__misshaialert?.error;
|
||||||
|
|
||||||
return error ? (
|
useEffect(() => {
|
||||||
<div className="container">
|
const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`);
|
||||||
<Header hasTopLink className="xarticle mb-2" />
|
const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches));
|
||||||
<div className="xarticle">
|
dispatch(setMobile(qMobile.matches));
|
||||||
<h1>{t('error')}</h1>
|
qMobile.addEventListener('change', syncMobile);
|
||||||
<p>{t('_error.sorry')}</p>
|
|
||||||
<p>
|
return () => {
|
||||||
{t('_error.additionalInfo')}
|
qMobile.removeEventListener('change', syncMobile);
|
||||||
{t(`_error.${error}`)}
|
};
|
||||||
</p>
|
}, []);
|
||||||
<Link to="/" onClick={() => (window as any).__misshaialert.error = null}>{t('retry')}</Link>
|
|
||||||
</div>
|
const TheLayout = session || $location.pathname !== '/' ? GeneralLayout : 'div';
|
||||||
</div>
|
|
||||||
) : (
|
return (
|
||||||
<div className="container">
|
<TheLayout>
|
||||||
{$location.pathname !== '/' && <Header hasTopLink className="xarticle mb-2" />}
|
{error ? (
|
||||||
<Switch>
|
<div>
|
||||||
<Route exact path="/" component={IndexPage} />
|
<h1>{t('error')}</h1>
|
||||||
<Route exact path="/ranking" component={RankingPage} />
|
<p>{t('_error.sorry')}</p>
|
||||||
<Route exact path="/term" component={TermPage} />
|
<p>
|
||||||
<Route exact path="/announcements/:id" component={AnnouncementPage} />
|
{t('_error.additionalInfo')}
|
||||||
</Switch>
|
{t(`_error.${error}`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : <Router />}
|
||||||
<footer className="text-center pa-5">
|
<footer className="text-center pa-5">
|
||||||
<p>(C)2020-2022 <a href={XELTICA_STUDIO_URL} target="_blank" rel="noopener noreferrer">Xeltica Studio</a></p>
|
<p>(C)2020-2022 <a href={XELTICA_STUDIO_URL} target="_blank" rel="noopener noreferrer">Xeltica Studio</a></p>
|
||||||
<p dangerouslySetInnerHTML={{__html: t('disclaimerForMisskeyHq')}} />
|
<p>
|
||||||
<p><Link to="/term">{t('termsOfService')}</Link></p>
|
<a href="https://xeltica.notion.site/Misskey-Tools-688187fc85de4b7e901055326c7ffe74" target="_blank" rel="noreferrer noopener">
|
||||||
{session && (
|
{t('termsOfService')}
|
||||||
<p>
|
</a>
|
||||||
<a
|
</p>
|
||||||
className="btn primary"
|
|
||||||
href={`https://${session.host}/share?text=${encodeURIComponent(t('shareMisskeyToolsNote') as string)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener">
|
|
||||||
{t('shareMisskeyTools')}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</footer>
|
</footer>
|
||||||
<ModalComponent />
|
<ModalComponent />
|
||||||
</div>
|
</TheLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
89
src/frontend/GeneralLayout.tsx
Normal file
89
src/frontend/GeneralLayout.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { NavigationMenu } from './components/NavigationMenu';
|
||||||
|
import { IsMobileProp } from './misc/is-mobile-prop';
|
||||||
|
|
||||||
|
import { useGetMetaQuery, useGetSessionQuery } from './services/session';
|
||||||
|
import { useSelector } from './store';
|
||||||
|
import { setDrawerShown } from './store/slices/screen';
|
||||||
|
|
||||||
|
const Container = styled.div<IsMobileProp>`
|
||||||
|
padding: var(--margin);
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sidebar = styled.nav`
|
||||||
|
width: 320px;
|
||||||
|
position: fixed;
|
||||||
|
top: var(--margin);
|
||||||
|
left: var(--margin);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Main = styled.main<IsMobileProp>`
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 80px;
|
||||||
|
margin-left: ${p => !p.isMobile ? `${320 + 16}px` : 0};
|
||||||
|
min-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MobileHeader = styled.header`
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 64px;
|
||||||
|
background: var(--panel);
|
||||||
|
> h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GeneralLayout: React.FC = ({children}) => {
|
||||||
|
const { data: session } = useGetSessionQuery(undefined);
|
||||||
|
const { data: meta } = useGetMetaQuery(undefined);
|
||||||
|
const { isMobile, title, isDrawerShown } = useSelector(state => state.screen);
|
||||||
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container isMobile={isMobile}>
|
||||||
|
{isMobile && (
|
||||||
|
<MobileHeader className="navbar hstack f-middle shadow-2 pl-2">
|
||||||
|
<button className="btn flat" onClick={() => dispatch(setDrawerShown(true))}>
|
||||||
|
<i className="fas fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<h1>{t(title ?? 'title')}</h1>
|
||||||
|
</MobileHeader>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{!isMobile && (
|
||||||
|
<Sidebar className="pa-2">
|
||||||
|
<NavigationMenu />
|
||||||
|
</Sidebar>
|
||||||
|
)}
|
||||||
|
<Main isMobile={isMobile}>
|
||||||
|
{session && meta && meta.currentTokenVersion !== session.tokenVersion && (
|
||||||
|
<div className="alert bg-danger flex f-middle mb-2">
|
||||||
|
<i className="icon fas fa-circle-exclamation"></i>
|
||||||
|
{t('shouldUpdateToken')}
|
||||||
|
<a className="btn primary" href={`/login?host=${encodeURIComponent(session.host)}`}>
|
||||||
|
{t('update')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Main>
|
||||||
|
</div>
|
||||||
|
<div className={`drawer-container ${isDrawerShown ? 'active' : ''}`}>
|
||||||
|
<div className="backdrop" onClick={() => dispatch(setDrawerShown(false))}></div>
|
||||||
|
<div className="drawer pa-2" onClick={e => e.stopPropagation()}>
|
||||||
|
<NavigationMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
31
src/frontend/Header.tsx
Normal file
31
src/frontend/Header.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useGetSessionQuery } from './services/session';
|
||||||
|
import { useSelector } from './store';
|
||||||
|
|
||||||
|
export type HeaderProps = {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({title}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data } = useGetSessionQuery(undefined);
|
||||||
|
const { isMobile } = useSelector(state => state.screen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="navbar hstack shadow-2 bg-panel rounded _header">
|
||||||
|
<h1 className="navbar-title text-primary mb-0 text-100">
|
||||||
|
{<Link to="/">{t('title')}</Link>}
|
||||||
|
{title && <> / {title}</>}
|
||||||
|
</h1>
|
||||||
|
{data && (
|
||||||
|
<button className="btn flat ml-auto primary">
|
||||||
|
<i className="fas fa-circle-user"></i>
|
||||||
|
{!isMobile && <span className="ml-1">{data.username}<span className="text-dimmed">@{data.host}</span></span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
|
@ -23,10 +23,10 @@ const getButtons = (button: DialogButtonType): DialogButton[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialogIconPattern: Record<DialogIcon, string> = {
|
const dialogIconPattern: Record<DialogIcon, string> = {
|
||||||
error: 'bi bi-x-circle-fill text-danger',
|
error: 'fas fa-circle-xmark text-danger',
|
||||||
info: 'bi bi-info-circle-fill text-primary',
|
info: 'fas fa-circle-info text-primary',
|
||||||
question: 'bi bi-question-circle-fill text-primary',
|
question: 'fas fa-circle-question text-primary',
|
||||||
warning: 'bi bi-exclamation-circle-fill text-warning',
|
warning: 'fas fa-circle-exclamation text-warning',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Dialog: React.VFC<{modal: ModalTypeDialog}> = ({modal}) => {
|
const Dialog: React.VFC<{modal: ModalTypeDialog}> = ({modal}) => {
|
||||||
|
|
25
src/frontend/Router.tsx
Normal file
25
src/frontend/Router.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
import { IndexPage } from './pages';
|
||||||
|
import { AnnouncementPage } from './pages/announcement';
|
||||||
|
import { RankingPage } from './pages/apps/misshai/ranking';
|
||||||
|
import { AdminPage } from './pages/admin';
|
||||||
|
import { AccountsPage } from './pages/account';
|
||||||
|
import { SettingPage } from './pages/settings';
|
||||||
|
import { MisshaiPage } from './pages/apps/misshai';
|
||||||
|
import { NekomimiPage } from './pages/apps/avatar-cropper';
|
||||||
|
|
||||||
|
export const Router: React.VFC = () => {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/" component={IndexPage} />
|
||||||
|
<Route exact path="/apps/avatar-cropper" component={NekomimiPage} />
|
||||||
|
<Route exact path="/apps/miss-hai" component={MisshaiPage} />
|
||||||
|
<Route exact path="/apps/miss-hai/ranking" component={RankingPage} />
|
||||||
|
<Route exact path="/announcements/:id" component={AnnouncementPage} />
|
||||||
|
<Route exact path="/account" component={AccountsPage} />
|
||||||
|
<Route exact path="/settings" component={SettingPage} />
|
||||||
|
<Route exact path="/admin" component={AdminPage} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
|
@ -24,7 +24,7 @@ export const AnnouncementList: React.VFC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="mb-0"><i className="bi-bell"></i> {t('announcements')}</h1>
|
<h1 className="mb-0"><i className="fas fa-bell"></i> {t('announcements')}</h1>
|
||||||
<div className="large menu fade">
|
<div className="large menu fade">
|
||||||
{announcements.map(a => (
|
{announcements.map(a => (
|
||||||
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
||||||
|
|
|
@ -5,6 +5,6 @@ import { Skeleton } from './Skeleton';
|
||||||
export const CurrentUser: React.VFC = () => {
|
export const CurrentUser: React.VFC = () => {
|
||||||
const {data} = useGetSessionQuery(undefined);
|
const {data} = useGetSessionQuery(undefined);
|
||||||
return data ? (
|
return data ? (
|
||||||
<h1 className="text-125"><i className="bi-person"></i> {data.username}<span className="text-dimmed">@{data.host}</span></h1>
|
<h1 className="text-125"><i className="fas fa-users"></i> {data.username}<span className="text-dimmed">@{data.host}</span></h1>
|
||||||
) : <Skeleton height="1.5rem" />;
|
) : <Skeleton height="1.5rem" />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,19 +5,19 @@ export const DeveloperInfo: React.VFC = () => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1><i className="bi-question-circle"></i> {t('_developerInfo.title')}</h1>
|
<h1><i className="fas fa-circle-question"></i> {t('_developerInfo.title')}</h1>
|
||||||
<p>{t('_developerInfo.description')}</p>
|
<p>{t('_developerInfo.description')}</p>
|
||||||
<div className="menu large">
|
<div className="menu large">
|
||||||
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
|
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
|
||||||
<i className="icon bi-at"></i>
|
<i className="icon fas fa-at"></i>
|
||||||
Lutica@groundpolis.app
|
Lutica@groundpolis.app
|
||||||
</a>
|
</a>
|
||||||
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
|
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
|
||||||
<i className="icon bi-at"></i>
|
<i className="icon fas fa-at"></i>
|
||||||
le@misskey.io
|
le@misskey.io
|
||||||
</a>
|
</a>
|
||||||
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
|
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
|
||||||
<i className="icon bi-at"></i>
|
<i className="icon fas fa-at"></i>
|
||||||
EbiseLutica@twitter.com
|
EbiseLutica@twitter.com
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { HTMLProps, useMemo, useState } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useGetMetaQuery, useGetSessionQuery } from '../services/session';
|
|
||||||
import { CHANGELOG_URL } from '../const';
|
|
||||||
import { createGacha } from '../../common/functions/create-gacha';
|
|
||||||
|
|
||||||
export type HeaderProps = {
|
|
||||||
hasTopLink?: boolean;
|
|
||||||
className?: HTMLProps<HTMLElement>['className'],
|
|
||||||
style?: HTMLProps<HTMLElement>['style'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({hasTopLink, children, className, style}) => {
|
|
||||||
const {data: meta} = useGetMetaQuery(undefined);
|
|
||||||
const {data: session} = useGetSessionQuery(undefined);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [generation, setGeneration] = useState(0);
|
|
||||||
const gacha = useMemo(() => createGacha(), [generation]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className={`card ${className ?? ''}`} style={style}>
|
|
||||||
<div className="body">
|
|
||||||
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
|
|
||||||
{hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')}
|
|
||||||
{meta && (
|
|
||||||
<a href={CHANGELOG_URL} target="_blank" rel="noopener noreferrer" className="text-125 text-dimmed ml-1">
|
|
||||||
v{meta?.version}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-dimmed">
|
|
||||||
<button onClick={() => setGeneration(g => g + 1)} className="text-primary">
|
|
||||||
<i className="bi bi-dice-5-fill" />
|
|
||||||
</button>
|
|
||||||
{gacha}
|
|
||||||
{session && (
|
|
||||||
<a
|
|
||||||
href={`https://${session.host}/share?text=${encodeURIComponent(`${gacha} https://misskey.tools`)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
className="ml-1">
|
|
||||||
<i className="bi bi-share-fill" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,5 +1,11 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const Input = styled.input`
|
||||||
|
width: auto;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
export const LoginForm: React.VFC = () => {
|
export const LoginForm: React.VFC = () => {
|
||||||
const [host, setHost] = useState('');
|
const [host, setHost] = useState('');
|
||||||
|
@ -11,7 +17,7 @@ export const LoginForm: React.VFC = () => {
|
||||||
<strong>{t('instanceUrl')}</strong>
|
<strong>{t('instanceUrl')}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="hgroup login-form">
|
<div className="hgroup login-form">
|
||||||
<input
|
<Input
|
||||||
className="input-field"
|
className="input-field"
|
||||||
type="text"
|
type="text"
|
||||||
value={host}
|
value={host}
|
||||||
|
|
64
src/frontend/components/NavigationMenu.tsx
Normal file
64
src/frontend/components/NavigationMenu.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { useGetSessionQuery } from '../services/session';
|
||||||
|
import { setDrawerShown } from '../store/slices/screen';
|
||||||
|
|
||||||
|
const navLinkClassName = (isActive: boolean) => `item ${isActive ? 'active' : ''}`;
|
||||||
|
|
||||||
|
export const NavigationMenu: React.VFC = () => {
|
||||||
|
const { data: session } = useGetSessionQuery(undefined);
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onClickItem = () => {
|
||||||
|
dispatch(setDrawerShown(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="text-175 text-dimmed mb-2">{t('title')}</h1>
|
||||||
|
<div className="menu">
|
||||||
|
<section>
|
||||||
|
<NavLink className={navLinkClassName} to="/" exact onClick={onClickItem}>
|
||||||
|
<i className={`icon fas fa-${session ? 'home' : 'arrow-left'}`}></i>
|
||||||
|
{t(session ? '_sidebar.dashboard' : '_sidebar.return')}
|
||||||
|
</NavLink>
|
||||||
|
</section>
|
||||||
|
{session && (
|
||||||
|
<section>
|
||||||
|
<h1>{t('_sidebar.tools')}</h1>
|
||||||
|
<NavLink className={navLinkClassName} to="/apps/miss-hai" onClick={onClickItem}>
|
||||||
|
<i className="icon fas fa-tower-broadcast"></i>
|
||||||
|
{t('_sidebar.missHaiAlert')}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className={navLinkClassName} to="/apps/avatar-cropper" onClick={onClickItem}>
|
||||||
|
<i className="icon fas fa-crop-simple"></i>
|
||||||
|
{t('_sidebar.cropper')}
|
||||||
|
</NavLink>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{session && (
|
||||||
|
<section>
|
||||||
|
<h1>{session.username}@{session.host}</h1>
|
||||||
|
<NavLink className={navLinkClassName} to="/account" onClick={onClickItem}>
|
||||||
|
<i className="icon fas fa-circle-user"></i>
|
||||||
|
{t('_sidebar.accounts')}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink className={navLinkClassName} to="/settings" onClick={onClickItem}>
|
||||||
|
<i className="icon fas fa-gear"></i>
|
||||||
|
{t('_sidebar.settings')}
|
||||||
|
</NavLink>
|
||||||
|
{session.isAdmin && (
|
||||||
|
<NavLink className={navLinkClassName} to="/admin" onClick={onClickItem}>
|
||||||
|
<i className="icon fas fa-lock"></i>
|
||||||
|
{t('_sidebar.admin')}
|
||||||
|
</NavLink>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -50,26 +50,37 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
|
||||||
{response.isCalculating ? (
|
{response.isCalculating ? (
|
||||||
<p>{t('isCalculating')}</p>
|
<p>{t('isCalculating')}</p>
|
||||||
) : (
|
) : (
|
||||||
<table className="table mt-1 fluid">
|
<div className="menu large">
|
||||||
<thead>
|
{response.ranking.map((r, i) => (
|
||||||
<tr>
|
<a href={`https://${r.host}/@${r.username}`} target="_blank" rel="noopener noreferrer nofollow" className="item flex" key={i}>
|
||||||
<th>{t('_missHai.order')}</th>
|
<div className="text-bold pr-2">{i + 1}</div>
|
||||||
<th>{t('name')}</th>
|
<div>
|
||||||
<th>{t('_missHai.rating')}</th>
|
{r.username}@{r.host}<br/>
|
||||||
</tr>
|
<span className="text-dimmed text-75">{t('_missHai.rating')}: {r.rating}</span>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</a>
|
||||||
{response.ranking.map((r, i) => (
|
))}
|
||||||
<tr key={i}>
|
</div>
|
||||||
<td>{i + 1}</td>
|
// <table className="table mt-1 fluid">
|
||||||
<td>
|
// <thead>
|
||||||
{r.username}@{r.host}
|
// <tr>
|
||||||
</td>
|
// <th>{t('_missHai.order')}</th>
|
||||||
<td>{r.rating}</td>
|
// <th>{t('name')}</th>
|
||||||
</tr>
|
// <th>{t('_missHai.rating')}</th>
|
||||||
))}
|
// </tr>
|
||||||
</tbody>
|
// </thead>
|
||||||
</table>
|
// <tbody>
|
||||||
|
// {response.ranking.map((r, i) => (
|
||||||
|
// <tr key={i}>
|
||||||
|
// <td>{i + 1}</td>
|
||||||
|
// <td>
|
||||||
|
// {r.username}@{r.host}
|
||||||
|
// </td>
|
||||||
|
// <td>{r.rating}</td>
|
||||||
|
// </tr>
|
||||||
|
// ))}
|
||||||
|
// </tbody>
|
||||||
|
// </table>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Ranking } from './Ranking';
|
|
||||||
|
|
||||||
export const RankingPage: React.VFC = () => {
|
|
||||||
const [limit, setLimit] = useState<number | undefined>(10);
|
|
||||||
const {t} = useTranslation();
|
|
||||||
return (
|
|
||||||
<div className="fade">
|
|
||||||
<Ranking limit={limit} />
|
|
||||||
{limit && <button className="btn link" onClick={() => setLimit(undefined)}>{t('_missHai.showAll')}</button>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -7,3 +7,5 @@ export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
||||||
|
|
||||||
export const CHANGELOG_URL = 'https://github.com/Xeltica/MisskeyTools/blob/master/CHANGELOG.md';
|
export const CHANGELOG_URL = 'https://github.com/Xeltica/MisskeyTools/blob/master/CHANGELOG.md';
|
||||||
export const XELTICA_STUDIO_URL = 'https://xeltica.work';
|
export const XELTICA_STUDIO_URL = 'https://xeltica.work';
|
||||||
|
|
||||||
|
export const BREAKPOINT_SM = '800px';
|
||||||
|
|
13
src/frontend/hooks/useTitle.ts
Normal file
13
src/frontend/hooks/useTitle.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { setTitle } from '../store/slices/screen';
|
||||||
|
|
||||||
|
export const useTitle = (title: string) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(setTitle(title));
|
||||||
|
return () => {
|
||||||
|
dispatch(setTitle(null));
|
||||||
|
};
|
||||||
|
}, [title]);
|
||||||
|
};
|
|
@ -2,10 +2,31 @@ import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import 'dayjs/locale/ja';
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import { getBrowserLanguage, resources } from './langs';
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
|
import { LOCALSTORAGE_KEY_LANG } from './const';
|
||||||
|
|
||||||
|
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
|
||||||
|
import './style.scss';
|
||||||
|
import 'dayjs/locale/ja';
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
if (!localStorage[LOCALSTORAGE_KEY_LANG]) {
|
||||||
|
localStorage[LOCALSTORAGE_KEY_LANG] = getBrowserLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources,
|
||||||
|
lng: localStorage[LOCALSTORAGE_KEY_LANG],
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false // Reactは常にXSS対策をしてくれるので、i18next側では対応不要
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ReactDOM.render(<App/>, document.getElementById('app'));
|
ReactDOM.render(<App/>, document.getElementById('app'));
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
import jaJP from './ja-JP.json';
|
import jaJP from './ja-JP.json';
|
||||||
import enUS from './en-US.json';
|
import enUS from './en-US.json';
|
||||||
import koKR from './ko-KR.json';
|
import koKR from './ko-KR.json';
|
||||||
|
import jaCR from './ja-cr.json';
|
||||||
|
|
||||||
|
import deepmerge from 'deepmerge';
|
||||||
|
|
||||||
|
const merge = (baseData: Record<string, unknown>, newData: Record<string, unknown>) => {
|
||||||
|
return deepmerge(baseData, newData, {
|
||||||
|
isMergeableObject: obj => typeof obj === 'object'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const resources = {
|
export const resources = {
|
||||||
'ja_JP': { translation: jaJP },
|
'ja_JP': { translation: jaJP },
|
||||||
'en_US': { translation: enUS },
|
'en_US': { translation: merge(jaJP, enUS) },
|
||||||
'ko_KR': { translation: koKR },
|
'ko_KR': { translation: merge(jaJP, koKR) },
|
||||||
|
'ja_CR': { translation: merge(jaJP, jaCR) },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const languageName = {
|
export const languageName = {
|
||||||
'ja_JP': '日本語',
|
'ja_JP': '日本語',
|
||||||
'en_US': 'English',
|
'en_US': 'English',
|
||||||
'ko_KR': '한국어',
|
'ko_KR': '한국어',
|
||||||
|
'ja_CR': '怪レい日本语',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type LanguageCode = keyof typeof resources;
|
export type LanguageCode = keyof typeof resources;
|
||||||
|
|
|
@ -49,6 +49,18 @@
|
||||||
"update": "更新する",
|
"update": "更新する",
|
||||||
"shareMisskeyTools": "#MisskeyTools をシェアする",
|
"shareMisskeyTools": "#MisskeyTools をシェアする",
|
||||||
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
|
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
|
||||||
|
"_sidebar": {
|
||||||
|
"dashboard": "ダッシュボード",
|
||||||
|
"tools": "ツール",
|
||||||
|
"manageTools": "ツールを管理",
|
||||||
|
"missHaiAlert": "ミス廃アラート",
|
||||||
|
"cropper": "ねこみみアジャスター",
|
||||||
|
"accounts": "アカウント",
|
||||||
|
"settings": "設定",
|
||||||
|
"admin": "管理画面",
|
||||||
|
"about": "Misskey Toolsについて",
|
||||||
|
"return": "トップページに戻る"
|
||||||
|
},
|
||||||
"_welcomeMessage": {
|
"_welcomeMessage": {
|
||||||
"pattern1": "ついついノートしすぎていませんか?",
|
"pattern1": "ついついノートしすぎていませんか?",
|
||||||
"pattern2": "Misskey, しすぎていませんか?",
|
"pattern2": "Misskey, しすぎていませんか?",
|
||||||
|
|
2
src/frontend/misc/is-mobile-prop.tsx
Normal file
2
src/frontend/misc/is-mobile-prop.tsx
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
export type IsMobileProp = { isMobile: boolean; };
|
|
@ -5,7 +5,7 @@ export interface ModalTypeMenu {
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItemClassName = `bi bi-${string}`;
|
export type MenuItemClassName = `fas fa-${string}`;
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
icon?: MenuItemClassName;
|
icon?: MenuItemClassName;
|
||||||
|
|
|
@ -5,14 +5,17 @@ import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
|
||||||
import { useGetSessionQuery } from '../services/session';
|
import { useGetSessionQuery } from '../services/session';
|
||||||
import { useSelector } from '../store';
|
import { useSelector } from '../store';
|
||||||
import { setAccounts } from '../store/slices/screen';
|
import { setAccounts } from '../store/slices/screen';
|
||||||
import { LoginForm } from './LoginForm';
|
import { LoginForm } from '../components/LoginForm';
|
||||||
import { Skeleton } from './Skeleton';
|
import { Skeleton } from '../components/Skeleton';
|
||||||
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
|
||||||
export const AccountsPage: React.VFC = () => {
|
export const AccountsPage: React.VFC = () => {
|
||||||
const {data} = useGetSessionQuery(undefined);
|
const {data} = useGetSessionQuery(undefined);
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useTitle('_sidebar.accounts');
|
||||||
|
|
||||||
const {accounts, accountTokens} = useSelector(state => state.screen);
|
const {accounts, accountTokens} = useSelector(state => state.screen);
|
||||||
|
|
||||||
const switchAccount = (token: string) => {
|
const switchAccount = (token: string) => {
|
||||||
|
@ -40,14 +43,14 @@ export const AccountsPage: React.VFC = () => {
|
||||||
accounts.length === accountTokens.length ? (
|
accounts.length === accountTokens.length ? (
|
||||||
accounts.map(account => (
|
accounts.map(account => (
|
||||||
<button className="item fluid" style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} onClick={() => switchAccount(account.misshaiToken)}>
|
<button className="item fluid" style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} onClick={() => switchAccount(account.misshaiToken)}>
|
||||||
<i className="icon bi bi-chevron-right" />
|
<i className="icon fas fa-chevron-right" />
|
||||||
@{account.username}@{account.host}
|
@{account.username}@{account.host}
|
||||||
<button className="btn flat text-danger" style={{marginLeft: 'auto'}} onClick={e => {
|
<button className="btn flat text-danger" style={{marginLeft: 'auto'}} onClick={e => {
|
||||||
const filteredAccounts = accounts.filter(ac => ac.id !== account.id);
|
const filteredAccounts = accounts.filter(ac => ac.id !== account.id);
|
||||||
dispatch(setAccounts(filteredAccounts));
|
dispatch(setAccounts(filteredAccounts));
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}>
|
}}>
|
||||||
<i className="bi bi-trash"/>
|
<i className="fas fa-trash-can"/>
|
||||||
</button>
|
</button>
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
|
@ -2,12 +2,12 @@ import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
|
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
|
||||||
import { useGetSessionQuery } from '../services/session';
|
import { useGetSessionQuery } from '../services/session';
|
||||||
import { Skeleton } from './Skeleton';
|
import { Skeleton } from '../components/Skeleton';
|
||||||
import { IAnnouncement } from '../../common/types/announcement';
|
import { IAnnouncement } from '../../common/types/announcement';
|
||||||
import { $delete, $get, $post, $put } from '../misc/api';
|
import { $delete, $get, $post, $put } from '../misc/api';
|
||||||
import { Card } from './Card';
|
|
||||||
import { showModal } from '../store/slices/screen';
|
import { showModal } from '../store/slices/screen';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
|
||||||
|
|
||||||
export const AdminPage: React.VFC = () => {
|
export const AdminPage: React.VFC = () => {
|
||||||
|
@ -15,6 +15,8 @@ export const AdminPage: React.VFC = () => {
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useTitle('_sidebar.admin');
|
||||||
|
|
||||||
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||||
const [selectedAnnouncement, selectAnnouncement] = useState<IAnnouncement | null>(null);
|
const [selectedAnnouncement, selectAnnouncement] = useState<IAnnouncement | null>(null);
|
||||||
const [isAnnouncementsLoaded, setAnnouncementsLoaded] = useState(false);
|
const [isAnnouncementsLoaded, setAnnouncementsLoaded] = useState(false);
|
||||||
|
@ -132,64 +134,66 @@ export const AdminPage: React.VFC = () => {
|
||||||
<p>You are not an administrator and cannot open this page.</p>
|
<p>You are not an administrator and cannot open this page.</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2>Announcements</h2>
|
<div className="card shadow-2">
|
||||||
{!isEditMode && (
|
<div className="body">
|
||||||
<label className="input-switch mb-1">
|
<h1>Announcements</h1>
|
||||||
<input type="checkbox" checked={isDeleteMode} onChange={e => setDeleteMode(e.target.checked)}/>
|
{!isEditMode && (
|
||||||
<div className="switch"></div>
|
<label className="input-switch mb-2">
|
||||||
<span>Delete Mode</span>
|
<input type="checkbox" checked={isDeleteMode} onChange={e => setDeleteMode(e.target.checked)}/>
|
||||||
</label>
|
<div className="switch"></div>
|
||||||
)}
|
<span>Delete Mode</span>
|
||||||
<Card bodyClassName={isEditMode ? '' : 'px-0'}>
|
</label>
|
||||||
{ !isEditMode ? (
|
)}
|
||||||
<>
|
{ !isEditMode ? (
|
||||||
{isDeleteMode && <div className="ml-2 text-danger">Click the item to delete.</div>}
|
<>
|
||||||
<div className="large menu">
|
{isDeleteMode && <div className="ml-2 text-danger">Click the item to delete.</div>}
|
||||||
{announcements.map(a => (
|
<div className="large menu">
|
||||||
<button className="item fluid" key={a.id} onClick={() => {
|
{announcements.map(a => (
|
||||||
if (isDeleteMode) {
|
<button className="item fluid" key={a.id} onClick={() => {
|
||||||
deleteAnnouncement(a);
|
if (isDeleteMode) {
|
||||||
} else {
|
deleteAnnouncement(a);
|
||||||
selectAnnouncement(a);
|
} else {
|
||||||
setEditMode(true);
|
selectAnnouncement(a);
|
||||||
}
|
setEditMode(true);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{isDeleteMode && <i className="icon bi fas fa-trash-can text-danger" />}
|
||||||
|
{a.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{!isDeleteMode && (
|
||||||
|
<button className="item fluid" onClick={() => setEditMode(true)}>
|
||||||
|
<i className="icon fas fa-plus"/ >
|
||||||
|
Create New
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="vstack">
|
||||||
|
<label className="input-field">
|
||||||
|
Title
|
||||||
|
<input type="text" value={draftTitle} onChange={e => setDraftTitle(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
<label className="input-field">
|
||||||
|
Body
|
||||||
|
<textarea className="input-field" value={draftBody} rows={10} onChange={e => setDraftBody(e.target.value)}/>
|
||||||
|
</label>
|
||||||
|
<div className="hstack" style={{justifyContent: 'flex-end'}}>
|
||||||
|
<button className="btn primary" onClick={submitAnnouncement} disabled={!draftTitle || !draftBody}>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
<button className="btn" onClick={() => {
|
||||||
|
selectAnnouncement(null);
|
||||||
|
setEditMode(false);
|
||||||
}}>
|
}}>
|
||||||
{isDeleteMode && <i className="icon bi bi-trash text-danger" />}
|
Cancel
|
||||||
{a.title}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
</div>
|
||||||
{!isDeleteMode && (
|
|
||||||
<button className="item fluid" onClick={() => setEditMode(true)}>
|
|
||||||
<i className="icon bi bi-plus"/ >
|
|
||||||
Create New
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
) : (
|
</div>
|
||||||
<div className="vstack">
|
</div>
|
||||||
<label className="input-field">
|
|
||||||
Title
|
|
||||||
<input type="text" value={draftTitle} onChange={e => setDraftTitle(e.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label className="input-field">
|
|
||||||
Body
|
|
||||||
<textarea className="input-field" value={draftBody} rows={10} onChange={e => setDraftBody(e.target.value)}/>
|
|
||||||
</label>
|
|
||||||
<div className="hstack" style={{justifyContent: 'flex-end'}}>
|
|
||||||
<button className="btn primary" onClick={submitAnnouncement} disabled={!draftTitle || !draftBody}>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
<button className="btn" onClick={() => {
|
|
||||||
selectAnnouncement(null);
|
|
||||||
setEditMode(false);
|
|
||||||
}}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
<h2>Misshai</h2>
|
<h2>Misshai</h2>
|
||||||
<div className="vstack">
|
<div className="vstack">
|
||||||
<button className="btn danger" onClick={onClickStartMisshaiAlertWorkerButton}>
|
<button className="btn danger" onClick={onClickStartMisshaiAlertWorkerButton}>
|
|
@ -6,6 +6,7 @@ import { IAnnouncement } from '../../common/types/announcement';
|
||||||
import { Skeleton } from '../components/Skeleton';
|
import { Skeleton } from '../components/Skeleton';
|
||||||
import { $get } from '../misc/api';
|
import { $get } from '../misc/api';
|
||||||
import { useSelector } from '../store';
|
import { useSelector } from '../store';
|
||||||
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
|
||||||
export const AnnouncementPage: React.VFC = () => {
|
export const AnnouncementPage: React.VFC = () => {
|
||||||
const { id } = useParams<{id: string}>();
|
const { id } = useParams<{id: string}>();
|
||||||
|
@ -15,6 +16,8 @@ export const AnnouncementPage: React.VFC = () => {
|
||||||
|
|
||||||
const lang = useSelector(state => state.screen.language);
|
const lang = useSelector(state => state.screen.language);
|
||||||
|
|
||||||
|
useTitle('announcements');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
|
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
|
||||||
}, [setAnnouncement]);
|
}, [setAnnouncement]);
|
||||||
|
@ -25,7 +28,7 @@ export const AnnouncementPage: React.VFC = () => {
|
||||||
<h2>
|
<h2>
|
||||||
{announcement.title}
|
{announcement.title}
|
||||||
<aside className="inline ml-1 text-dimmed text-100">
|
<aside className="inline ml-1 text-dimmed text-100">
|
||||||
<i className="bi bi-clock" />
|
<i className="fas fa-clock" />
|
||||||
{dayjs(announcement.createdAt).locale(lang.split('_')[0]).fromNow()}
|
{dayjs(announcement.createdAt).locale(lang.split('_')[0]).fromNow()}
|
||||||
</aside>
|
</aside>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
11
src/frontend/pages/announcements/index.tsx
Normal file
11
src/frontend/pages/announcements/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useTitle } from '../../hooks/useTitle';
|
||||||
|
|
||||||
|
export const AnnouncementsPage: React.VFC = () => {
|
||||||
|
useTitle('announcements');
|
||||||
|
return (
|
||||||
|
<div className="fade">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -2,10 +2,12 @@ import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ReactCrop, { Crop } from 'react-image-crop';
|
import ReactCrop, { Crop } from 'react-image-crop';
|
||||||
|
|
||||||
import 'react-image-crop/dist/ReactCrop.css';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useGetSessionQuery } from '../services/session';
|
import { useGetSessionQuery } from '../../services/session';
|
||||||
import { showModal } from '../store/slices/screen';
|
import { showModal } from '../../store/slices/screen';
|
||||||
|
|
||||||
|
import 'react-image-crop/dist/ReactCrop.css';
|
||||||
|
import { useTitle } from '../../hooks/useTitle';
|
||||||
|
|
||||||
export const NekomimiPage: React.VFC = () => {
|
export const NekomimiPage: React.VFC = () => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
@ -19,6 +21,8 @@ export const NekomimiPage: React.VFC = () => {
|
||||||
const [crop, setCrop] = useState<Partial<Crop>>({unit: '%', width: 100, aspect: 1 / 1});
|
const [crop, setCrop] = useState<Partial<Crop>>({unit: '%', width: 100, aspect: 1 / 1});
|
||||||
const [completedCrop, setCompletedCrop] = useState<Crop>();
|
const [completedCrop, setCompletedCrop] = useState<Crop>();
|
||||||
|
|
||||||
|
useTitle('catAdjuster');
|
||||||
|
|
||||||
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const {data} = useGetSessionQuery(undefined);
|
const {data} = useGetSessionQuery(undefined);
|
|
@ -3,11 +3,11 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--margin);
|
gap: var(--margin);
|
||||||
> .misshaiData {
|
> .misshaiData {
|
||||||
flex: 3 0 300px;
|
|
||||||
}
|
|
||||||
> .developerInfo {
|
|
||||||
flex: 1 0 300px;
|
flex: 1 0 300px;
|
||||||
}
|
}
|
||||||
|
> .misshaiRanking {
|
||||||
|
flex: 4 0 300px;
|
||||||
|
}
|
||||||
> .alertModeSetting {
|
> .alertModeSetting {
|
||||||
flex: 1 0 300px;
|
flex: 1 0 300px;
|
||||||
}
|
}
|
|
@ -2,19 +2,18 @@ import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { alertModes } from '../../common/types/alert-mode';
|
import { alertModes } from '../../../common/types/alert-mode';
|
||||||
import { IUser } from '../../common/types/user';
|
import { IUser } from '../../../common/types/user';
|
||||||
import { Visibility } from '../../common/types/visibility';
|
import { Visibility } from '../../../common/types/visibility';
|
||||||
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
|
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../../const';
|
||||||
import { $post, $put } from '../misc/api';
|
import { $post, $put } from '../../misc/api';
|
||||||
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
import { useGetScoreQuery, useGetSessionQuery } from '../../services/session';
|
||||||
import { showModal } from '../store/slices/screen';
|
import { showModal } from '../../store/slices/screen';
|
||||||
import { AnnouncementList } from './AnnouncementList';
|
import { Skeleton } from '../../components/Skeleton';
|
||||||
import { Ranking } from './Ranking';
|
|
||||||
import { Skeleton } from './Skeleton';
|
|
||||||
|
|
||||||
import './MisshaiPage.scss';
|
import './misshai.scss';
|
||||||
import { DeveloperInfo } from './DeveloperInfo';
|
import { Ranking } from '../../components/Ranking';
|
||||||
|
import { useTitle } from '../../hooks/useTitle';
|
||||||
|
|
||||||
const variables = [
|
const variables = [
|
||||||
'notesCount',
|
'notesCount',
|
||||||
|
@ -43,14 +42,15 @@ type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
|
||||||
|
|
||||||
export const MisshaiPage: React.VFC = () => {
|
export const MisshaiPage: React.VFC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [limit, setLimit] = useState<number | undefined>(10);
|
|
||||||
|
|
||||||
const session = useGetSessionQuery(undefined);
|
const session = useGetSessionQuery(undefined);
|
||||||
|
const [limit, setLimit] = useState<number | undefined>(10);
|
||||||
const data = session.data;
|
const data = session.data;
|
||||||
const score = useGetScoreQuery(undefined);
|
const score = useGetScoreQuery(undefined);
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
useTitle('_sidebar.missHaiAlert');
|
||||||
|
|
||||||
const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => {
|
const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => {
|
||||||
return { ...state, ...action };
|
return { ...state, ...action };
|
||||||
}, {
|
}, {
|
||||||
|
@ -195,54 +195,42 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
<Skeleton width="100%" height="160px" />
|
<Skeleton width="100%" height="160px" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="vstack">
|
<div className="vstack fade">
|
||||||
<div className="card announcement">
|
<div className="card misshaiData">
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<AnnouncementList />
|
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
||||||
</div>
|
<table className="table fluid">
|
||||||
</div>
|
<thead>
|
||||||
<div className="misshaiPageLayout">
|
<tr>
|
||||||
<div className="card misshaiData">
|
<th></th>
|
||||||
<div className="body">
|
<th>{t('_missHai.dataScore')}</th>
|
||||||
<h1><i className="bi bi-activity"></i> {t('_missHai.data')}</h1>
|
<th>{t('_missHai.dataDelta')}</th>
|
||||||
<table className="table fluid">
|
</tr>
|
||||||
<thead>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<th></th>
|
<tr>
|
||||||
<th>{t('_missHai.dataScore')}</th>
|
<td>{t('notes')}</td>
|
||||||
<th>{t('_missHai.dataDelta')}</th>
|
<td>{score.data.notesCount}</td>
|
||||||
</tr>
|
<td>{score.data.notesDelta}</td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<td>{t('following')}</td>
|
||||||
<td>{t('notes')}</td>
|
<td>{score.data.followingCount}</td>
|
||||||
<td>{score.data.notesCount}</td>
|
<td>{score.data.followingDelta}</td>
|
||||||
<td>{score.data.notesDelta}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<td>{t('followers')}</td>
|
||||||
<td>{t('following')}</td>
|
<td>{score.data.followersCount}</td>
|
||||||
<td>{score.data.followingCount}</td>
|
<td>{score.data.followersDelta}</td>
|
||||||
<td>{score.data.followingDelta}</td>
|
</tr>
|
||||||
</tr>
|
</tbody>
|
||||||
<tr>
|
</table>
|
||||||
<td>{t('followers')}</td>
|
<p>
|
||||||
<td>{score.data.followersCount}</td>
|
<strong>
|
||||||
<td>{score.data.followersDelta}</td>
|
{t('_missHai.rating')}{': '}
|
||||||
</tr>
|
</strong>
|
||||||
</tbody>
|
{session.data.rating}
|
||||||
</table>
|
</p>
|
||||||
<p>
|
|
||||||
<strong>
|
|
||||||
{t('_missHai.rating')}{': '}
|
|
||||||
</strong>
|
|
||||||
{session.data.rating}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card developerInfo">
|
|
||||||
<div className="body">
|
|
||||||
<DeveloperInfo />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card misshaiRanking">
|
<div className="card misshaiRanking">
|
||||||
|
@ -261,8 +249,8 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
<div className="misshaiPageLayout">
|
<div className="misshaiPageLayout">
|
||||||
<div className="card alertModeSetting">
|
<div className="card alertModeSetting">
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<h1 className="mb-2"><i className="bi bi-gear"></i> {t('alertMode')}</h1>
|
<h1><i className="fas fa-gear"></i> {t('alertMode')}</h1>
|
||||||
<div className="vstack">
|
<div className="vstack slim">
|
||||||
{ alertModes.map((mode) => (
|
{ alertModes.map((mode) => (
|
||||||
<label key={mode} className="input-check">
|
<label key={mode} className="input-check">
|
||||||
<input type="radio" checked={mode === draft.alertMode} onChange={() => {
|
<input type="radio" checked={mode === draft.alertMode} onChange={() => {
|
||||||
|
@ -274,14 +262,14 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
</div>
|
</div>
|
||||||
{ (draft.alertMode === 'notification' || draft.alertMode === 'both') && (
|
{ (draft.alertMode === 'notification' || draft.alertMode === 'both') && (
|
||||||
<div className="alert bg-danger mt-2">
|
<div className="alert bg-danger mt-2">
|
||||||
<i className="icon bi bi-exclamation-circle"></i>
|
<i className="icon fas fa-circle-exclamation"></i>
|
||||||
{t('_alertMode.notificationWarning')}
|
{t('_alertMode.notificationWarning')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
||||||
<>
|
<>
|
||||||
<h2 className="mt-2 mb-1">{t('visibility')}</h2>
|
<h2 className="mt-2 mb-1">{t('visibility')}</h2>
|
||||||
<div className="vstack">
|
<div className="vstack slim">
|
||||||
{
|
{
|
||||||
availableVisibilities.map((visibility) => (
|
availableVisibilities.map((visibility) => (
|
||||||
<label key={visibility} className="input-check">
|
<label key={visibility} className="input-check">
|
||||||
|
@ -305,15 +293,15 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="card templateSetting">
|
<div className="card templateSetting">
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<h1><i className="bi-card-text"></i> {t('template')}</h1>
|
<h1><i className="fas fa-pen-to-square"></i> {t('template')}</h1>
|
||||||
<p>{t('_template.description')}</p>
|
<p>{t('_template.description')}</p>
|
||||||
<div className="hstack dense mb-2">
|
<div className="hstack dense mb-2">
|
||||||
<button className="btn" onClick={onClickInsertVariables}>
|
<button className="btn" onClick={onClickInsertVariables}>
|
||||||
<i className="bi bi-braces" />
|
{'{ } '}
|
||||||
{t('_template.insertVariables')}
|
{t('_template.insertVariables')}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
|
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
|
||||||
<i className="bi bi-question-circle" />
|
<i className="fas fa-circle-question" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea ref={templateTextarea} className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => {
|
<textarea ref={templateTextarea} className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => {
|
||||||
|
@ -331,7 +319,7 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="list-form mt-2">
|
<div className="list-form mt-2">
|
||||||
<button className="item" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>
|
<button className="item" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>
|
||||||
<i className="icon bi bi-send" />
|
<i className="icon fas fa-paper-plane" />
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<h1>{t('sendAlert')}</h1>
|
<h1>{t('sendAlert')}</h1>
|
||||||
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
|
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Ranking } from '../components/Ranking';
|
import { Ranking } from '../../../components/Ranking';
|
||||||
|
import { useTitle } from '../../../hooks/useTitle';
|
||||||
|
|
||||||
|
|
||||||
export const RankingPage: React.VFC = () => {
|
export const RankingPage: React.VFC = () => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
useTitle('_missHai.ranking');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="xarticle">
|
<article className="xarticle">
|
|
@ -1,77 +1,77 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { Header } from '../components/Header';
|
|
||||||
import { MisshaiPage } from '../components/MisshaiPage';
|
|
||||||
import { Tab, TabItem } from '../components/Tab';
|
|
||||||
import { SettingPage } from '../components/SettingPage';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AccountsPage } from '../components/AccountsPage';
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
|
import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
|
||||||
import { IUser } from '../../common/types/user';
|
import { IUser } from '../../common/types/user';
|
||||||
import { setAccounts } from '../store/slices/screen';
|
import { setAccounts } from '../store/slices/screen';
|
||||||
import { useGetMetaQuery, useGetSessionQuery } from '../services/session';
|
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
||||||
import { AdminPage } from '../components/AdminPage';
|
|
||||||
import { $get } from '../misc/api';
|
import { $get } from '../misc/api';
|
||||||
import { NekomimiPage } from '../components/NekomimiPage';
|
import { AnnouncementList } from '../components/AnnouncementList';
|
||||||
import { Card } from '../components/Card';
|
import { DeveloperInfo } from '../components/DeveloperInfo';
|
||||||
import { CurrentUser } from '../components/CurrentUser';
|
|
||||||
|
|
||||||
export const IndexSessionPage: React.VFC = () => {
|
export const IndexSessionPage: React.VFC = () => {
|
||||||
const [selectedTab, setSelectedTab] = useState<string>('misshai');
|
const {t} = useTranslation();
|
||||||
const {t, i18n} = useTranslation();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { data: session } = useGetSessionQuery(undefined);
|
const { data: session } = useGetSessionQuery(undefined);
|
||||||
const { data: meta } = useGetMetaQuery(undefined);
|
const score = useGetScoreQuery(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
|
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
|
||||||
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
|
Promise.all(accounts.map(token => $get<IUser>('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const items = useMemo<TabItem[]>(() => {
|
|
||||||
const it: TabItem[] = [];
|
|
||||||
it.push({ label: t('_nav.misshai'), key: 'misshai' });
|
|
||||||
it.push({ label: t('_nav.accounts'), key: 'accounts' });
|
|
||||||
it.push({ label: t('_nav.catAdjuster'), key: 'nekomimi' });
|
|
||||||
if (session?.isAdmin) {
|
|
||||||
it.push({ label: 'Admin', key: 'admin' });
|
|
||||||
}
|
|
||||||
it.push({ label: t('_nav.settings'), key: 'settings' });
|
|
||||||
return it;
|
|
||||||
}, [i18n.language, session]);
|
|
||||||
|
|
||||||
const component = useMemo(() => {
|
|
||||||
switch (selectedTab) {
|
|
||||||
case 'misshai': return <MisshaiPage />;
|
|
||||||
case 'accounts': return <AccountsPage />;
|
|
||||||
case 'admin': return <AdminPage />;
|
|
||||||
case 'nekomimi': return <NekomimiPage />;
|
|
||||||
case 'settings': return <SettingPage/>;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}, [selectedTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="vstack fade">
|
||||||
<div className="xarticle vgroup shadow-4" style={{position: 'sticky', top: 0, zIndex: 100}}>
|
<div className="card announcement">
|
||||||
<Header />
|
<div className="body">
|
||||||
<div className="card">
|
<AnnouncementList />
|
||||||
<Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="xarticle mt-2">
|
<div className="misshaiPageLayout">
|
||||||
<Card className="mb-2">
|
<div className="card misshaiData">
|
||||||
<CurrentUser/>
|
<div className="body">
|
||||||
{session && meta && meta.currentTokenVersion !== session.tokenVersion && (
|
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
||||||
<div className="text-danger mt-1">
|
<table className="table fluid">
|
||||||
{t('shouldUpdateToken')}
|
<thead>
|
||||||
<a href={`/login?host=${encodeURIComponent(session.host)}`}>{t('update')}</a>
|
<tr>
|
||||||
</div>
|
<th></th>
|
||||||
)}
|
<th>{t('_missHai.dataScore')}</th>
|
||||||
</Card>
|
<th>{t('_missHai.dataDelta')}</th>
|
||||||
{component}
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{t('notes')}</td>
|
||||||
|
<td>{score.data?.notesCount ?? '...'}</td>
|
||||||
|
<td>{score.data?.notesDelta ?? '...'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t('following')}</td>
|
||||||
|
<td>{score.data?.followingCount ?? '...'}</td>
|
||||||
|
<td>{score.data?.followingDelta ?? '...'}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t('followers')}</td>
|
||||||
|
<td>{score.data?.followersCount ?? '...'}</td>
|
||||||
|
<td>{score.data?.followersDelta ?? '...'}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
{t('_missHai.rating')}{': '}
|
||||||
|
</strong>
|
||||||
|
{session?.rating ?? '...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card developerInfo">
|
||||||
|
<div className="body">
|
||||||
|
<DeveloperInfo />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +1,122 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { LoginForm } from '../components/LoginForm';
|
import { LoginForm } from '../components/LoginForm';
|
||||||
import { Header } from '../components/Header';
|
import styled from 'styled-components';
|
||||||
import { AnnouncementList } from '../components/AnnouncementList';
|
import { useSelector } from '../store';
|
||||||
|
import { IsMobileProp } from '../misc/is-mobile-prop';
|
||||||
|
import { IAnnouncement } from '../../common/types/announcement';
|
||||||
|
import { $get } from '../misc/api';
|
||||||
|
import Twemoji from 'react-twemoji';
|
||||||
|
|
||||||
|
const Hero = styled.div<IsMobileProp>`
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(-135deg, rgb(1, 169, 46), rgb(134, 179, 0) 35%);
|
||||||
|
color: var(--white);
|
||||||
|
padding: ${f => f.isMobile ? '16px' : '60px 90px'};
|
||||||
|
overflow: hidden;
|
||||||
|
gap: var(--margin);
|
||||||
|
> .hero {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
p {
|
||||||
|
${f => f.isMobile ? 'font-size: 1rem;' : ''}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .announcements {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
max-height: 512px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--margin);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--black-50);
|
||||||
|
backdrop-filter: blur(4px) brightness(120%);
|
||||||
|
z-index: 1000;
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .rects {
|
||||||
|
position: absolute;
|
||||||
|
display: grid;
|
||||||
|
right: 160px;
|
||||||
|
bottom: -120px;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
transform-origin: center center;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
opacity: 0.5;
|
||||||
|
> .rect {
|
||||||
|
border: 2px solid var(--white);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 2px 4px var(--shadow-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FormWrapper = styled.div`
|
||||||
|
max-width: 500px;
|
||||||
|
color: var(--fg);
|
||||||
|
`;
|
||||||
|
|
||||||
export const IndexWelcomePage: React.VFC = () => {
|
export const IndexWelcomePage: React.VFC = () => {
|
||||||
|
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||||
|
|
||||||
|
const {isMobile} = useSelector(state => state.screen);
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
const fetchAllAnnouncements = () => {
|
||||||
|
setAnnouncements([]);
|
||||||
|
$get<IAnnouncement[]>('announcements').then(announcements => {
|
||||||
|
setAnnouncements(announcements ?? []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllAnnouncements();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header className="xarticle mb-4">
|
<Hero className="fluid shadow-2" isMobile={isMobile}>
|
||||||
<article className="mt-4">
|
<div className="hero">
|
||||||
<p>{t('description1')}</p>
|
<h1 className="shadow-t">{t('title')}</h1>
|
||||||
<p>{t('description2')}</p>
|
<p className="shadow-t">{t('description1')}</p>
|
||||||
</article>
|
<p className="shadow-t">{t('description2')}</p>
|
||||||
<LoginForm />
|
<FormWrapper className="bg-panel pa-2 mt-4 rounded shadow-2">
|
||||||
</Header>
|
<LoginForm />
|
||||||
<article className="xarticle card">
|
</FormWrapper>
|
||||||
<div className="body">
|
|
||||||
<AnnouncementList />
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
<div className="announcements">
|
||||||
<hr />
|
<h2>お知らせ</h2>
|
||||||
|
<div className="menu large">
|
||||||
|
{announcements.map(a => (
|
||||||
|
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
||||||
|
{a.title}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rects">
|
||||||
|
<div className="rect"></div>
|
||||||
|
<div className="rect"></div>
|
||||||
|
<div className="rect"></div>
|
||||||
|
<div className="rect"></div>
|
||||||
|
</div>
|
||||||
|
</Hero>
|
||||||
|
<Twemoji options={{className: 'twemoji'}}>
|
||||||
|
<div className="py-4 text-125 text-center">
|
||||||
|
👍 ❤ 😆 🎉 🍮
|
||||||
|
</div>
|
||||||
|
</Twemoji>
|
||||||
<article className="xarticle vstack pa-2">
|
<article className="xarticle vstack pa-2">
|
||||||
<header>
|
<header>
|
||||||
<h2>{t('_welcome.title')}</h2>
|
<h2>{t('_welcome.title')}</h2>
|
||||||
|
@ -30,18 +124,18 @@ export const IndexWelcomePage: React.VFC = () => {
|
||||||
</header>
|
</header>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<article className="col-4 col-12-sm">
|
<article className="col-4 col-12-sm">
|
||||||
<h3><i className="bi bi-megaphone-fill"/> {t('_welcome.misshaiAlertTitle')}</h3>
|
<h3><i className="fas fa-bullhorn"/> {t('_welcome.misshaiAlertTitle')}</h3>
|
||||||
<p>{t('_welcome.misshaiAlertDescription')}</p>
|
<p>{t('_welcome.misshaiAlertDescription')}</p>
|
||||||
</article>
|
</article>
|
||||||
<article className="col-4 col-12-sm">
|
<article className="col-4 col-12-sm">
|
||||||
<h3><i className="bi bi-bar-chart-fill"/> {t('_missHai.ranking')}</h3>
|
<h3><i className="fas fa-ranking-star"/> {t('_missHai.ranking')}</h3>
|
||||||
<p>{t('_welcome.misshaiRankingDescription')}</p>
|
<p>{t('_welcome.misshaiRankingDescription')}</p>
|
||||||
<Link to="/ranking">{t('_missHai.showRanking')}</Link>
|
<Link to="/apps/miss-hai/ranking">{t('_missHai.showRanking')}</Link>
|
||||||
</article>
|
</article>
|
||||||
<div className="col-4 col-12-sm">
|
<article className="col-4 col-12-sm">
|
||||||
<h3><i className="bi bi-crop"/> {t('catAdjuster')}</h3>
|
<h3><i className="fas fa-crop-simple"/> {t('catAdjuster')}</h3>
|
||||||
<p>{t('_welcome.catAdjusterDescription')}</p>
|
<p>{t('_welcome.catAdjusterDescription')}</p>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<article className="mt-5">
|
<article className="mt-5">
|
||||||
<h3>{t('_welcome.nextFeaturesTitle')}</h3>
|
<h3>{t('_welcome.nextFeaturesTitle')}</h3>
|
||||||
|
|
|
@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import { useGetSessionQuery } from '../services/session';
|
import { useGetSessionQuery } from '../services/session';
|
||||||
import { Card } from './Card';
|
import { Card } from '../components/Card';
|
||||||
import { Theme, themes } from '../misc/theme';
|
import { Theme, themes } from '../misc/theme';
|
||||||
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
|
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
|
||||||
import { changeLang, changeTheme, showModal } from '../store/slices/screen';
|
import { changeLang, changeTheme, showModal } from '../store/slices/screen';
|
||||||
import { useSelector } from '../store';
|
import { useSelector } from '../store';
|
||||||
import { languageName } from '../langs';
|
import { languageName } from '../langs';
|
||||||
import { $delete } from '../misc/api';
|
import { $delete } from '../misc/api';
|
||||||
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
|
||||||
export const SettingPage: React.VFC = () => {
|
export const SettingPage: React.VFC = () => {
|
||||||
const session = useGetSessionQuery(undefined);
|
const session = useGetSessionQuery(undefined);
|
||||||
|
@ -18,6 +19,8 @@ export const SettingPage: React.VFC = () => {
|
||||||
const data = session.data;
|
const data = session.data;
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
|
useTitle('_sidebar.settings');
|
||||||
|
|
||||||
const currentTheme = useSelector(state => state.screen.theme);
|
const currentTheme = useSelector(state => state.screen.theme);
|
||||||
const currentLang = useSelector(state => state.screen.language);
|
const currentLang = useSelector(state => state.screen.language);
|
||||||
|
|
||||||
|
@ -39,7 +42,7 @@ export const SettingPage: React.VFC = () => {
|
||||||
onSelect(i) {
|
onSelect(i) {
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
|
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
|
||||||
location.reload();
|
location.href = '/';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
@ -69,7 +72,7 @@ export const SettingPage: React.VFC = () => {
|
||||||
message: t('_deactivate.success'),
|
message: t('_deactivate.success'),
|
||||||
icon: 'info',
|
icon: 'info',
|
||||||
onSelect() {
|
onSelect() {
|
||||||
location.reload();
|
location.href = '/';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -91,7 +94,7 @@ export const SettingPage: React.VFC = () => {
|
||||||
) : (
|
) : (
|
||||||
<div className="vstack fade">
|
<div className="vstack fade">
|
||||||
<Card bodyClassName="vstack">
|
<Card bodyClassName="vstack">
|
||||||
<h1><i className="bi bi-palette"></i> {t('appearance')}</h1>
|
<h1><i className="fas fa-palette"></i> {t('appearance')}</h1>
|
||||||
<h2>{t('theme')}</h2>
|
<h2>{t('theme')}</h2>
|
||||||
<div className="vstack">
|
<div className="vstack">
|
||||||
{
|
{
|
||||||
|
@ -115,21 +118,23 @@ export const SettingPage: React.VFC = () => {
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<div className="alert bg-info mt-2">
|
<div className="alert bg-info mt-2">
|
||||||
<i className="icon bi bi-translate" />
|
<i className="icon fas fa-language" />
|
||||||
{t('translatedByTheCommunity')}
|
<div>
|
||||||
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
|
{t('translatedByTheCommunity')}
|
||||||
|
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="list-form">
|
<div className="list-form">
|
||||||
<button className="item" onClick={onClickLogout}>
|
<button className="item" onClick={onClickLogout}>
|
||||||
<i className="icon bi bi-box-arrow-right" />
|
<i className="icon fas fa-arrow-up-right-from-square" />
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<h1>{t('logout')}</h1>
|
<h1>{t('logout')}</h1>
|
||||||
<p className="desc">{t('logoutDescription')}</p>
|
<p className="desc">{t('logoutDescription')}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button className="item text-danger" onClick={onClickDeleteAccount}>
|
<button className="item text-danger" onClick={onClickDeleteAccount}>
|
||||||
<i className="icon bi bi-trash" />
|
<i className="icon fas fa-trash-can" />
|
||||||
<div className="body">
|
<div className="body">
|
||||||
<h1>{t('deleteAccount')}</h1>
|
<h1>{t('deleteAccount')}</h1>
|
||||||
<p className="desc">{t('deleteAccountDescription')}</p>
|
<p className="desc">{t('deleteAccountDescription')}</p>
|
|
@ -1,35 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export const TermPage: React.VFC = () => {
|
|
||||||
// TODO: 外部サイトに誘導する
|
|
||||||
return (
|
|
||||||
<article className="xarticle">
|
|
||||||
<h2>利用規約</h2>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
本サービスは「現状のまま」「無保証」で提供されます。本サービスを利用したことによる損害など
|
|
||||||
について、管理人は一切責任を負わないものとします。
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
本サービスは、Misskey プロジェクトとは無関係です。
|
|
||||||
本サービスに関して Misskey プロジェクトに問い合わせる行為はお控え下さい。
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
ユーザーはインスタンスの諸規約に従った上で本サービスを使うものとします。
|
|
||||||
インスタンスの規約により自動投稿が禁止されている場合は本サービスを使用しないでください。
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
本サービスでは、接続先のアカウントが存在しない、トークンが失効してしまったなどの場合に、
|
|
||||||
自動的にユーザーアカウントを削除します。
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
運営は、正当な理由がある場合に限り、本サービスを特定のインスタンスで利用できないよう
|
|
||||||
ブロックする可能性があります。
|
|
||||||
</li>
|
|
||||||
<li>本サービスの仕様は、事前の予告無しに変更される可能性があります。</li>
|
|
||||||
<li>本サービスは、事前の予告無しに突然閉鎖される可能性があります。</li>
|
|
||||||
<li>本規約は、事前の予告無しに変更される可能性があります。</li>
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
import { WritableDraft } from 'immer/dist/internal';
|
||||||
import { IUser } from '../../../common/types/user';
|
import { IUser } from '../../../common/types/user';
|
||||||
|
|
||||||
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
||||||
|
@ -10,9 +11,12 @@ interface ScreenState {
|
||||||
modal: Modal | null;
|
modal: Modal | null;
|
||||||
modalShown: boolean;
|
modalShown: boolean;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
title: string | null;
|
||||||
language: string;
|
language: string;
|
||||||
accounts: IUser[];
|
accounts: IUser[];
|
||||||
accountTokens: string[];
|
accountTokens: string[];
|
||||||
|
isMobile: boolean;
|
||||||
|
isDrawerShown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ScreenState = {
|
const initialState: ScreenState = {
|
||||||
|
@ -20,8 +24,21 @@ const initialState: ScreenState = {
|
||||||
modalShown: false,
|
modalShown: false,
|
||||||
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
|
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
|
||||||
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
|
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
|
||||||
|
title: null,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
||||||
|
isMobile: false,
|
||||||
|
isDrawerShown: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 値を設定するReducerを生成します。
|
||||||
|
*/
|
||||||
|
const generateSetter = <T extends keyof WritableDraft<ScreenState>>(key: T, callback?: (state: WritableDraft<ScreenState>, action: PayloadAction<ScreenState[T]>) => void) => {
|
||||||
|
return (state: WritableDraft<ScreenState>, action: PayloadAction<ScreenState[T]>) => {
|
||||||
|
state[key] = action.payload;
|
||||||
|
if (callback) callback(state, action);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const screenSlice = createSlice({
|
export const screenSlice = createSlice({
|
||||||
|
@ -36,23 +53,23 @@ export const screenSlice = createSlice({
|
||||||
state.modal = null;
|
state.modal = null;
|
||||||
state.modalShown = false;
|
state.modalShown = false;
|
||||||
},
|
},
|
||||||
changeTheme: (state, action: PayloadAction<ScreenState['theme']>) => {
|
changeTheme: generateSetter('theme', (_, action) => {
|
||||||
state.theme = action.payload;
|
|
||||||
localStorage[LOCALSTORAGE_KEY_THEME] = action.payload;
|
localStorage[LOCALSTORAGE_KEY_THEME] = action.payload;
|
||||||
},
|
}),
|
||||||
changeLang: (state, action: PayloadAction<ScreenState['language']>) => {
|
changeLang: generateSetter('language', (_, action) => {
|
||||||
state.language = action.payload;
|
|
||||||
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
||||||
i18n.changeLanguage(action.payload);
|
i18n.changeLanguage(action.payload);
|
||||||
},
|
}),
|
||||||
setAccounts: (state, action: PayloadAction<ScreenState['accounts']>) => {
|
setAccounts: generateSetter('accounts', (state, action) => {
|
||||||
state.accounts = action.payload;
|
|
||||||
state.accountTokens = action.payload.map(a => a.misshaiToken);
|
state.accountTokens = action.payload.map(a => a.misshaiToken);
|
||||||
localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens);
|
localStorage[LOCALSTORAGE_KEY_ACCOUNTS] = JSON.stringify(state.accountTokens);
|
||||||
},
|
}),
|
||||||
|
setMobile: generateSetter('isMobile'),
|
||||||
|
setTitle: generateSetter('title'),
|
||||||
|
setDrawerShown: generateSetter('isDrawerShown'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { showModal, hideModal, changeTheme, changeLang, setAccounts } = screenSlice.actions;
|
export const { showModal, hideModal, changeTheme, changeLang, setAccounts, setMobile, setTitle, setDrawerShown } = screenSlice.actions;
|
||||||
|
|
||||||
export default screenSlice.reducer;
|
export default screenSlice.reducer;
|
||||||
|
|
|
@ -19,6 +19,15 @@ hr {
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
._header {
|
||||||
|
position: sticky;
|
||||||
|
top: var(--container-padding);
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
top: var(--container-padding-phone);
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.fade {
|
.fade {
|
||||||
animation: 0.3s ease-out 0s fadeIn;
|
animation: 0.3s ease-out 0s fadeIn;
|
||||||
&.down {
|
&.down {
|
||||||
|
@ -94,6 +103,10 @@ small {
|
||||||
max-width: min(100vw, 600px);
|
max-width: min(100vw, 600px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card > .body > h1 {
|
||||||
|
margin-bottom: var(--margin);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-menu-wrapper {
|
.modal-menu-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -131,3 +144,9 @@ small {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.twemoji {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
vertical-align: -0.1em;
|
||||||
|
}
|
87
yarn.lock
87
yarn.lock
|
@ -355,6 +355,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/ms" "*"
|
"@types/ms" "*"
|
||||||
|
|
||||||
|
"@types/deepmerge@^2.2.0":
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/deepmerge/-/deepmerge-2.2.0.tgz#6f63896c217f3164782f52d858d9f3a927139f64"
|
||||||
|
integrity sha512-FEQYDHh6+Q+QXKSrIY46m+/lAmAj/bk4KpLaam+hArmzaVpMBHLcfwOH2Q2UOkWM7XsdY9PmZpGyPAjh/JRGhQ==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "*"
|
||||||
|
|
||||||
"@types/eslint-scope@^3.7.0":
|
"@types/eslint-scope@^3.7.0":
|
||||||
version "3.7.1"
|
version "3.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
|
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
|
||||||
|
@ -615,6 +622,13 @@
|
||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-twemoji@^0.4.0":
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-twemoji/-/react-twemoji-0.4.0.tgz#3f3ce96e273ec0aa4b4ddca2913951b168fd72ee"
|
||||||
|
integrity sha512-OVkDaNTg9WqqM2MBqL68FNPsn+5aabQIbL9KY+ofK/Q4ENOuaHOWsg/jRD9zQ+GX5L+7LC1Ztgr4iK0/qZd17w==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^17.0.19":
|
"@types/react@*", "@types/react@^17.0.19":
|
||||||
version "17.0.27"
|
version "17.0.27"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.27.tgz#6498ed9b3ad117e818deb5525fa1946c09f2e0e6"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.27.tgz#6498ed9b3ad117e818deb5525fa1946c09f2e0e6"
|
||||||
|
@ -1737,6 +1751,11 @@ deep-is@^0.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||||
|
|
||||||
|
deepmerge@*, deepmerge@^4.2.2:
|
||||||
|
version "4.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||||
|
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
|
||||||
|
|
||||||
defer-to-connect@^1.0.1:
|
defer-to-connect@^1.0.1:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
|
resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
|
||||||
|
@ -2364,6 +2383,15 @@ fresh@0.5.2, fresh@~0.5.2:
|
||||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
||||||
|
|
||||||
|
fs-extra@^8.0.1:
|
||||||
|
version "8.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||||
|
integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.2.0"
|
||||||
|
jsonfile "^4.0.0"
|
||||||
|
universalify "^0.1.0"
|
||||||
|
|
||||||
fs.realpath@^1.0.0:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
|
@ -2534,6 +2562,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
|
||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
||||||
|
|
||||||
|
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||||
|
version "4.2.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||||
|
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||||
|
|
||||||
has-ansi@^2.0.0:
|
has-ansi@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||||
|
@ -3183,6 +3216,22 @@ json5@^2.1.2, json5@^2.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
jsonfile@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
|
||||||
|
integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
|
jsonfile@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922"
|
||||||
|
integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==
|
||||||
|
dependencies:
|
||||||
|
universalify "^0.1.2"
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs "^4.1.6"
|
||||||
|
|
||||||
jstransformer@1.0.0:
|
jstransformer@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
|
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
|
||||||
|
@ -3416,6 +3465,11 @@ lodash.clonedeep@^4.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
||||||
|
|
||||||
|
lodash.isequal@^4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||||
|
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||||
|
|
||||||
lodash.merge@^4.6.2:
|
lodash.merge@^4.6.2:
|
||||||
version "4.6.2"
|
version "4.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||||
|
@ -4810,6 +4864,15 @@ react-router@5.2.1:
|
||||||
tiny-invariant "^1.0.2"
|
tiny-invariant "^1.0.2"
|
||||||
tiny-warning "^1.0.0"
|
tiny-warning "^1.0.0"
|
||||||
|
|
||||||
|
react-twemoji@^0.5.0:
|
||||||
|
version "0.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-twemoji/-/react-twemoji-0.5.0.tgz#0565f8e427fc4c9ef3680977c4a88fbdef79f874"
|
||||||
|
integrity sha512-xz3NLWTFCfWOmPd559jcFX4f976ORIPpL9SwdBQO5BZwIYD1U1vpbY2E6k2vwPCVH78s2m1GbG5jpHKGUPZ+gw==
|
||||||
|
dependencies:
|
||||||
|
lodash.isequal "^4.5.0"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
twemoji "14.0.1"
|
||||||
|
|
||||||
react@^17.0.2:
|
react@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
@ -5750,6 +5813,21 @@ tsutils@^3.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "^1.8.1"
|
tslib "^1.8.1"
|
||||||
|
|
||||||
|
twemoji-parser@14.0.0:
|
||||||
|
version "14.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-14.0.0.tgz#13dabcb6d3a261d9efbf58a1666b182033bf2b62"
|
||||||
|
integrity sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==
|
||||||
|
|
||||||
|
twemoji@14.0.1:
|
||||||
|
version "14.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-14.0.1.tgz#0640887ef149403ae577081cbc2480a026e55ed6"
|
||||||
|
integrity sha512-eoqhea0sUhmC10iTacksyp1v9O4BP1jKmVqtK+Nztw40/dzawSHkXL3/xCpyh+mukmEvJ0Gw9VLvwZfQ9HKXDw==
|
||||||
|
dependencies:
|
||||||
|
fs-extra "^8.0.1"
|
||||||
|
jsonfile "^5.0.0"
|
||||||
|
twemoji-parser "14.0.0"
|
||||||
|
universalify "^0.1.2"
|
||||||
|
|
||||||
type-check@^0.4.0, type-check@~0.4.0:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
|
@ -5925,6 +6003,11 @@ unist-util-visit@^4.0.0:
|
||||||
unist-util-is "^5.0.0"
|
unist-util-is "^5.0.0"
|
||||||
unist-util-visit-parents "^5.0.0"
|
unist-util-visit-parents "^5.0.0"
|
||||||
|
|
||||||
|
universalify@^0.1.0, universalify@^0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
unpipe@1.0.0, unpipe@~1.0.0:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
@ -6228,8 +6311,8 @@ xdg-basedir@^4.0.0:
|
||||||
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||||
|
|
||||||
xeltica-ui@xeltica/design-system:
|
xeltica-ui@xeltica/design-system:
|
||||||
version "1.0.0-beta.7"
|
version "1.0.0-beta.9"
|
||||||
resolved "https://codeload.github.com/xeltica/design-system/tar.gz/d0b49b6487c379a10f6e568c0f9e37b0035ea7f2"
|
resolved "https://codeload.github.com/xeltica/design-system/tar.gz/83b7faede9b0a42a9a3bd8d462d95484cfa67294"
|
||||||
|
|
||||||
xml2js@^0.4.17:
|
xml2js@^0.4.17:
|
||||||
version "0.4.23"
|
version "0.4.23"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue