diff --git a/package.json b/package.json
index 2c56673..f68b147 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dependencies": {
"@babel/preset-react": "^7.14.5",
"@reduxjs/toolkit": "^1.6.1",
+ "@types/deepmerge": "^2.2.0",
"@types/insert-text-at-cursor": "^0.3.0",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-multer": "^1.0.0",
@@ -38,6 +39,7 @@
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
+ "@types/react-twemoji": "^0.4.0",
"@types/styled-components": "^5.1.13",
"@types/uuid": "^8.0.0",
"axios": "^0.21.2",
@@ -45,6 +47,7 @@
"class-validator": "^0.13.1",
"css-loader": "^6.2.0",
"dayjs": "^1.10.7",
+ "deepmerge": "^4.2.2",
"delay": "^4.4.0",
"fibers": "^5.0.0",
"i18next": "^20.6.1",
@@ -73,6 +76,7 @@
"react-modal-hook": "^3.0.0",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.1",
+ "react-twemoji": "^0.5.0",
"reflect-metadata": "^0.1.13",
"rndstr": "^1.0.0",
"routing-controllers": "^0.9.0",
diff --git a/src/backend/views/frontend.pug b/src/backend/views/frontend.pug
index fd638fd..94f7f0c 100644
--- a/src/backend/views/frontend.pug
+++ b/src/backend/views/frontend.pug
@@ -15,7 +15,7 @@ html
meta(name='twitter:site' content='@Xeltica')
meta(name='twitter:creator' content='@Xeltica')
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.
.loading {
display: flex;
diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx
index d906625..3b08b17 100644
--- a/src/frontend/App.tsx
+++ b/src/frontend/App.tsx
@@ -1,91 +1,64 @@
-import React from 'react';
-import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom';
-import { Provider } from 'react-redux';
+import React, { useEffect } from 'react';
+import { BrowserRouter, useLocation } from 'react-router-dom';
+import { Provider, useDispatch } from 'react-redux';
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 { ModalComponent } from './Modal';
import { useTheme } from './misc/theme';
-import { getBrowserLanguage, resources } from './langs';
-import { LOCALSTORAGE_KEY_LANG, XELTICA_STUDIO_URL } from './const';
+import { BREAKPOINT_SM, XELTICA_STUDIO_URL } from './const';
import { useGetSessionQuery } from './services/session';
-import { AnnouncementPage } from './pages/announcement';
-
-import 'xeltica-ui/dist/css/xeltica-ui.min.css';
-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
- }
- });
+import { Router } from './Router';
+import { setMobile } from './store/slices/screen';
+import { GeneralLayout } from './GeneralLayout';
const AppInner : React.VFC = () => {
const { data: session } = useGetSessionQuery(undefined);
const $location = useLocation();
+ const dispatch = useDispatch();
+
useTheme();
const {t} = useTranslation();
const error = (window as any).__misshaialert?.error;
- return error ? (
-
-
-
-
{t('error')}
-
{t('_error.sorry')}
-
- {t('_error.additionalInfo')}
- {t(`_error.${error}`)}
-
-
(window as any).__misshaialert.error = null}>{t('retry')}
-
-
- ) : (
-
- {$location.pathname !== '/' &&
}
-
-
-
-
-
-
+ useEffect(() => {
+ const qMobile = window.matchMedia(`(max-width: ${BREAKPOINT_SM})`);
+ const syncMobile = (ev: MediaQueryListEvent) => dispatch(setMobile(ev.matches));
+ dispatch(setMobile(qMobile.matches));
+ qMobile.addEventListener('change', syncMobile);
+
+ return () => {
+ qMobile.removeEventListener('change', syncMobile);
+ };
+ }, []);
+
+ const TheLayout = session || $location.pathname !== '/' ? GeneralLayout : 'div';
+
+ return (
+
+ {error ? (
+
+
{t('error')}
+
{t('_error.sorry')}
+
+ {t('_error.additionalInfo')}
+ {t(`_error.${error}`)}
+
+
+ ) : }
-
+
);
};
diff --git a/src/frontend/GeneralLayout.tsx b/src/frontend/GeneralLayout.tsx
new file mode 100644
index 0000000..7808c74
--- /dev/null
+++ b/src/frontend/GeneralLayout.tsx
@@ -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`
+ padding: var(--margin);
+ position: relative;
+`;
+
+const Sidebar = styled.nav`
+ width: 320px;
+ position: fixed;
+ top: var(--margin);
+ left: var(--margin);
+`;
+
+const Main = styled.main`
+ 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 (
+
+ {isMobile && (
+
+
+ {t(title ?? 'title')}
+
+ )}
+
+ {!isMobile && (
+
+
+
+ )}
+
+ {session && meta && meta.currentTokenVersion !== session.tokenVersion && (
+
+ )}
+ {children}
+
+
+
+
dispatch(setDrawerShown(false))}>
+
e.stopPropagation()}>
+
+
+
+
+ );
+};
diff --git a/src/frontend/Header.tsx b/src/frontend/Header.tsx
new file mode 100644
index 0000000..a122828
--- /dev/null
+++ b/src/frontend/Header.tsx
@@ -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 = ({title}) => {
+ const { t } = useTranslation();
+ const { data } = useGetSessionQuery(undefined);
+ const { isMobile } = useSelector(state => state.screen);
+
+ return (
+
+
+ {{t('title')}}
+ {title && <> / {title}>}
+
+ {data && (
+
+ )}
+
+ );
+};
diff --git a/src/frontend/Modal.tsx b/src/frontend/Modal.tsx
index 5cf3275..c6db066 100644
--- a/src/frontend/Modal.tsx
+++ b/src/frontend/Modal.tsx
@@ -23,10 +23,10 @@ const getButtons = (button: DialogButtonType): DialogButton[] => {
};
const dialogIconPattern: Record = {
- error: 'bi bi-x-circle-fill text-danger',
- info: 'bi bi-info-circle-fill text-primary',
- question: 'bi bi-question-circle-fill text-primary',
- warning: 'bi bi-exclamation-circle-fill text-warning',
+ error: 'fas fa-circle-xmark text-danger',
+ info: 'fas fa-circle-info text-primary',
+ question: 'fas fa-circle-question text-primary',
+ warning: 'fas fa-circle-exclamation text-warning',
};
const Dialog: React.VFC<{modal: ModalTypeDialog}> = ({modal}) => {
diff --git a/src/frontend/Router.tsx b/src/frontend/Router.tsx
new file mode 100644
index 0000000..ed7692d
--- /dev/null
+++ b/src/frontend/Router.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/frontend/components/AnnouncementList.tsx b/src/frontend/components/AnnouncementList.tsx
index 93f3279..04f8b20 100644
--- a/src/frontend/components/AnnouncementList.tsx
+++ b/src/frontend/components/AnnouncementList.tsx
@@ -24,7 +24,7 @@ export const AnnouncementList: React.VFC = () => {
return (
<>
- {t('announcements')}
+ {t('announcements')}
{announcements.map(a => (
diff --git a/src/frontend/components/CurrentUser.tsx b/src/frontend/components/CurrentUser.tsx
index 0c5b00c..4d88e93 100644
--- a/src/frontend/components/CurrentUser.tsx
+++ b/src/frontend/components/CurrentUser.tsx
@@ -5,6 +5,6 @@ import { Skeleton } from './Skeleton';
export const CurrentUser: React.VFC = () => {
const {data} = useGetSessionQuery(undefined);
return data ? (
-
{data.username}@{data.host}
+
{data.username}@{data.host}
) :
;
};
diff --git a/src/frontend/components/DeveloperInfo.tsx b/src/frontend/components/DeveloperInfo.tsx
index 26fe6f9..f371f99 100644
--- a/src/frontend/components/DeveloperInfo.tsx
+++ b/src/frontend/components/DeveloperInfo.tsx
@@ -5,19 +5,19 @@ export const DeveloperInfo: React.VFC = () => {
const {t} = useTranslation();
return (
<>
-
{t('_developerInfo.title')}
+
{t('_developerInfo.title')}
{t('_developerInfo.description')}
diff --git a/src/frontend/components/Header.tsx b/src/frontend/components/Header.tsx
deleted file mode 100644
index 05869d6..0000000
--- a/src/frontend/components/Header.tsx
+++ /dev/null
@@ -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
['className'],
- style?: HTMLProps['style'],
-};
-
-export const Header: React.FC = ({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 (
-
-
-
- {hasTopLink ? {t('title')} : t('title')}
- {meta && (
-
- v{meta?.version}
-
- )}
-
-
-
- {gacha}
- {session && (
-
-
-
- )}
-
- {children}
-
-
- );
-};
diff --git a/src/frontend/components/LoginForm.tsx b/src/frontend/components/LoginForm.tsx
index 7a3aed2..250ba9b 100644
--- a/src/frontend/components/LoginForm.tsx
+++ b/src/frontend/components/LoginForm.tsx
@@ -1,5 +1,11 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
+import styled from 'styled-components';
+
+const Input = styled.input`
+ width: auto;
+ flex: 1;
+`;
export const LoginForm: React.VFC = () => {
const [host, setHost] = useState('');
@@ -11,7 +17,7 @@ export const LoginForm: React.VFC = () => {
{t('instanceUrl')}
-
`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 (
+ <>
+
{t('title')}
+
+
+
+
+ {t(session ? '_sidebar.dashboard' : '_sidebar.return')}
+
+
+ {session && (
+
+ {t('_sidebar.tools')}
+
+
+ {t('_sidebar.missHaiAlert')}
+
+
+
+ {t('_sidebar.cropper')}
+
+
+ )}
+ {session && (
+
+ {session.username}@{session.host}
+
+
+ {t('_sidebar.accounts')}
+
+
+
+ {t('_sidebar.settings')}
+
+ {session.isAdmin && (
+
+
+ {t('_sidebar.admin')}
+
+ )}
+
+ )}
+
+ >
+ );
+};
diff --git a/src/frontend/components/Ranking.tsx b/src/frontend/components/Ranking.tsx
index 17616fc..79bc5ab 100644
--- a/src/frontend/components/Ranking.tsx
+++ b/src/frontend/components/Ranking.tsx
@@ -50,26 +50,37 @@ export const Ranking: React.VFC
= ({limit}) => {
{response.isCalculating ? (
{t('isCalculating')}
) : (
-
-
-
- {t('_missHai.order')} |
- {t('name')} |
- {t('_missHai.rating')} |
-
-
-
- {response.ranking.map((r, i) => (
-
- {i + 1} |
-
- {r.username}@{r.host}
- |
- {r.rating} |
-
- ))}
-
-
+
+ //
+ //
+ //
+ // {t('_missHai.order')} |
+ // {t('name')} |
+ // {t('_missHai.rating')} |
+ //
+ //
+ //
+ // {response.ranking.map((r, i) => (
+ //
+ // {i + 1} |
+ //
+ // {r.username}@{r.host}
+ // |
+ // {r.rating} |
+ //
+ // ))}
+ //
+ //
)}
>
) : null
diff --git a/src/frontend/components/RankingPage.tsx b/src/frontend/components/RankingPage.tsx
deleted file mode 100644
index e9a9e63..0000000
--- a/src/frontend/components/RankingPage.tsx
+++ /dev/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(10);
- const {t} = useTranslation();
- return (
-
-
- {limit && }
-
- );
-};
diff --git a/src/frontend/const.ts b/src/frontend/const.ts
index 1983a42..e0ad2cb 100644
--- a/src/frontend/const.ts
+++ b/src/frontend/const.ts
@@ -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 XELTICA_STUDIO_URL = 'https://xeltica.work';
+
+export const BREAKPOINT_SM = '800px';
diff --git a/src/frontend/hooks/useTitle.ts b/src/frontend/hooks/useTitle.ts
new file mode 100644
index 0000000..8a997bf
--- /dev/null
+++ b/src/frontend/hooks/useTitle.ts
@@ -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]);
+};
diff --git a/src/frontend/init.tsx b/src/frontend/init.tsx
index fb5e79f..e1e2a77 100644
--- a/src/frontend/init.tsx
+++ b/src/frontend/init.tsx
@@ -2,10 +2,31 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import relativeTime from 'dayjs/plugin/relativeTime';
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 { LOCALSTORAGE_KEY_LANG } from './const';
+
+import 'xeltica-ui/dist/css/xeltica-ui.min.css';
+import './style.scss';
+import 'dayjs/locale/ja';
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(, document.getElementById('app'));
diff --git a/src/frontend/langs/index.ts b/src/frontend/langs/index.ts
index ec8fa0d..514b284 100644
--- a/src/frontend/langs/index.ts
+++ b/src/frontend/langs/index.ts
@@ -1,17 +1,28 @@
import jaJP from './ja-JP.json';
import enUS from './en-US.json';
import koKR from './ko-KR.json';
+import jaCR from './ja-cr.json';
+
+import deepmerge from 'deepmerge';
+
+const merge = (baseData: Record, newData: Record) => {
+ return deepmerge(baseData, newData, {
+ isMergeableObject: obj => typeof obj === 'object'
+ });
+};
export const resources = {
'ja_JP': { translation: jaJP },
- 'en_US': { translation: enUS },
- 'ko_KR': { translation: koKR },
+ 'en_US': { translation: merge(jaJP, enUS) },
+ 'ko_KR': { translation: merge(jaJP, koKR) },
+ 'ja_CR': { translation: merge(jaJP, jaCR) },
};
export const languageName = {
'ja_JP': '日本語',
'en_US': 'English',
'ko_KR': '한국어',
+ 'ja_CR': '怪レい日本语',
} as const;
export type LanguageCode = keyof typeof resources;
diff --git a/src/frontend/langs/ja-JP.json b/src/frontend/langs/ja-JP.json
index 92d45af..6f038ba 100644
--- a/src/frontend/langs/ja-JP.json
+++ b/src/frontend/langs/ja-JP.json
@@ -49,6 +49,18 @@
"update": "更新する",
"shareMisskeyTools": "#MisskeyTools をシェアする",
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
+ "_sidebar": {
+ "dashboard": "ダッシュボード",
+ "tools": "ツール",
+ "manageTools": "ツールを管理",
+ "missHaiAlert": "ミス廃アラート",
+ "cropper": "ねこみみアジャスター",
+ "accounts": "アカウント",
+ "settings": "設定",
+ "admin": "管理画面",
+ "about": "Misskey Toolsについて",
+ "return": "トップページに戻る"
+ },
"_welcomeMessage": {
"pattern1": "ついついノートしすぎていませんか?",
"pattern2": "Misskey, しすぎていませんか?",
diff --git a/src/frontend/misc/is-mobile-prop.tsx b/src/frontend/misc/is-mobile-prop.tsx
new file mode 100644
index 0000000..ca299fc
--- /dev/null
+++ b/src/frontend/misc/is-mobile-prop.tsx
@@ -0,0 +1,2 @@
+
+export type IsMobileProp = { isMobile: boolean; };
diff --git a/src/frontend/modal/menu.ts b/src/frontend/modal/menu.ts
index bce02a6..e65e87a 100644
--- a/src/frontend/modal/menu.ts
+++ b/src/frontend/modal/menu.ts
@@ -5,7 +5,7 @@ export interface ModalTypeMenu {
items: MenuItem[];
}
-export type MenuItemClassName = `bi bi-${string}`;
+export type MenuItemClassName = `fas fa-${string}`;
export interface MenuItem {
icon?: MenuItemClassName;
diff --git a/src/frontend/components/AccountsPage.tsx b/src/frontend/pages/account.tsx
similarity index 87%
rename from src/frontend/components/AccountsPage.tsx
rename to src/frontend/pages/account.tsx
index 8b6e371..8660f31 100644
--- a/src/frontend/components/AccountsPage.tsx
+++ b/src/frontend/pages/account.tsx
@@ -5,14 +5,17 @@ import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetSessionQuery } from '../services/session';
import { useSelector } from '../store';
import { setAccounts } from '../store/slices/screen';
-import { LoginForm } from './LoginForm';
-import { Skeleton } from './Skeleton';
+import { LoginForm } from '../components/LoginForm';
+import { Skeleton } from '../components/Skeleton';
+import { useTitle } from '../hooks/useTitle';
export const AccountsPage: React.VFC = () => {
const {data} = useGetSessionQuery(undefined);
const {t} = useTranslation();
const dispatch = useDispatch();
+ useTitle('_sidebar.accounts');
+
const {accounts, accountTokens} = useSelector(state => state.screen);
const switchAccount = (token: string) => {
@@ -40,14 +43,14 @@ export const AccountsPage: React.VFC = () => {
accounts.length === accountTokens.length ? (
accounts.map(account => (
))
diff --git a/src/frontend/components/AdminPage.tsx b/src/frontend/pages/admin.tsx
similarity index 64%
rename from src/frontend/components/AdminPage.tsx
rename to src/frontend/pages/admin.tsx
index a2cd132..8a3efa2 100644
--- a/src/frontend/components/AdminPage.tsx
+++ b/src/frontend/pages/admin.tsx
@@ -2,12 +2,12 @@ import React, { useEffect, useState } from 'react';
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetSessionQuery } from '../services/session';
-import { Skeleton } from './Skeleton';
+import { Skeleton } from '../components/Skeleton';
import { IAnnouncement } from '../../common/types/announcement';
import { $delete, $get, $post, $put } from '../misc/api';
-import { Card } from './Card';
import { showModal } from '../store/slices/screen';
import { useDispatch } from 'react-redux';
+import { useTitle } from '../hooks/useTitle';
export const AdminPage: React.VFC = () => {
@@ -15,6 +15,8 @@ export const AdminPage: React.VFC = () => {
const dispatch = useDispatch();
+ useTitle('_sidebar.admin');
+
const [announcements, setAnnouncements] = useState([]);
const [selectedAnnouncement, selectAnnouncement] = useState(null);
const [isAnnouncementsLoaded, setAnnouncementsLoaded] = useState(false);
@@ -132,64 +134,66 @@ export const AdminPage: React.VFC = () => {
You are not an administrator and cannot open this page.
) : (
<>
- Announcements
- {!isEditMode && (
-
- )}
-
- { !isEditMode ? (
- <>
- {isDeleteMode && Click the item to delete.
}
-
- {announcements.map(a => (
-
{
- if (isDeleteMode) {
- deleteAnnouncement(a);
- } else {
- selectAnnouncement(a);
- setEditMode(true);
- }
+
+
+
Announcements
+ {!isEditMode && (
+
+ )}
+ { !isEditMode ? (
+ <>
+ {isDeleteMode &&
Click the item to delete.
}
+
+ {announcements.map(a => (
+ {
+ if (isDeleteMode) {
+ deleteAnnouncement(a);
+ } else {
+ selectAnnouncement(a);
+ setEditMode(true);
+ }
+ }}>
+ {isDeleteMode && }
+ {a.title}
+
+ ))}
+ {!isDeleteMode && (
+ setEditMode(true)}>
+
+ Create New
+
+ )}
+
+ >
+ ) : (
+
+
+
+
+
+ Submit
+
+ {
+ selectAnnouncement(null);
+ setEditMode(false);
}}>
- {isDeleteMode && }
- {a.title}
+ Cancel
- ))}
- {!isDeleteMode && (
- setEditMode(true)}>
-
- Create New
-
- )}
+
- >
- ) : (
-
-
-
-
-
- Submit
-
- {
- selectAnnouncement(null);
- setEditMode(false);
- }}>
- Cancel
-
-
-
- )}
-
+ )}
+
+
Misshai
diff --git a/src/frontend/pages/announcement.tsx b/src/frontend/pages/announcement.tsx
index c52940c..0122796 100644
--- a/src/frontend/pages/announcement.tsx
+++ b/src/frontend/pages/announcement.tsx
@@ -6,6 +6,7 @@ import { IAnnouncement } from '../../common/types/announcement';
import { Skeleton } from '../components/Skeleton';
import { $get } from '../misc/api';
import { useSelector } from '../store';
+import { useTitle } from '../hooks/useTitle';
export const AnnouncementPage: React.VFC = () => {
const { id } = useParams<{id: string}>();
@@ -15,6 +16,8 @@ export const AnnouncementPage: React.VFC = () => {
const lang = useSelector(state => state.screen.language);
+ useTitle('announcements');
+
useEffect(() => {
$get('announcements/' + id).then(setAnnouncement);
}, [setAnnouncement]);
@@ -25,7 +28,7 @@ export const AnnouncementPage: React.VFC = () => {
{announcement.title}
diff --git a/src/frontend/pages/announcements/index.tsx b/src/frontend/pages/announcements/index.tsx
new file mode 100644
index 0000000..83e93ed
--- /dev/null
+++ b/src/frontend/pages/announcements/index.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { useTitle } from '../../hooks/useTitle';
+
+export const AnnouncementsPage: React.VFC = () => {
+ useTitle('announcements');
+ return (
+
+
+
+ );
+};
diff --git a/src/frontend/components/NekomimiPage.tsx b/src/frontend/pages/apps/avatar-cropper.tsx
similarity index 95%
rename from src/frontend/components/NekomimiPage.tsx
rename to src/frontend/pages/apps/avatar-cropper.tsx
index edca50a..fd2a78e 100644
--- a/src/frontend/components/NekomimiPage.tsx
+++ b/src/frontend/pages/apps/avatar-cropper.tsx
@@ -2,10 +2,12 @@ import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactCrop, { Crop } from 'react-image-crop';
-import 'react-image-crop/dist/ReactCrop.css';
import { useDispatch } from 'react-redux';
-import { useGetSessionQuery } from '../services/session';
-import { showModal } from '../store/slices/screen';
+import { useGetSessionQuery } from '../../services/session';
+import { showModal } from '../../store/slices/screen';
+
+import 'react-image-crop/dist/ReactCrop.css';
+import { useTitle } from '../../hooks/useTitle';
export const NekomimiPage: React.VFC = () => {
const {t} = useTranslation();
@@ -19,6 +21,8 @@ export const NekomimiPage: React.VFC = () => {
const [crop, setCrop] = useState>({unit: '%', width: 100, aspect: 1 / 1});
const [completedCrop, setCompletedCrop] = useState();
+ useTitle('catAdjuster');
+
const previewCanvasRef = useRef(null);
const {data} = useGetSessionQuery(undefined);
diff --git a/src/frontend/components/MisshaiPage.scss b/src/frontend/pages/apps/misshai.scss
similarity index 83%
rename from src/frontend/components/MisshaiPage.scss
rename to src/frontend/pages/apps/misshai.scss
index bb36fe1..4ad80dc 100644
--- a/src/frontend/components/MisshaiPage.scss
+++ b/src/frontend/pages/apps/misshai.scss
@@ -3,11 +3,11 @@
flex-wrap: wrap;
gap: var(--margin);
> .misshaiData {
- flex: 3 0 300px;
- }
- > .developerInfo {
flex: 1 0 300px;
}
+ > .misshaiRanking {
+ flex: 4 0 300px;
+ }
> .alertModeSetting {
flex: 1 0 300px;
}
diff --git a/src/frontend/components/MisshaiPage.tsx b/src/frontend/pages/apps/misshai.tsx
similarity index 78%
rename from src/frontend/components/MisshaiPage.tsx
rename to src/frontend/pages/apps/misshai.tsx
index eb4c246..7c72438 100644
--- a/src/frontend/components/MisshaiPage.tsx
+++ b/src/frontend/pages/apps/misshai.tsx
@@ -2,19 +2,18 @@ import insertTextAtCursor from 'insert-text-at-cursor';
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
-import { alertModes } from '../../common/types/alert-mode';
-import { IUser } from '../../common/types/user';
-import { Visibility } from '../../common/types/visibility';
-import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../const';
-import { $post, $put } from '../misc/api';
-import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
-import { showModal } from '../store/slices/screen';
-import { AnnouncementList } from './AnnouncementList';
-import { Ranking } from './Ranking';
-import { Skeleton } from './Skeleton';
+import { alertModes } from '../../../common/types/alert-mode';
+import { IUser } from '../../../common/types/user';
+import { Visibility } from '../../../common/types/visibility';
+import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_TOKEN } from '../../const';
+import { $post, $put } from '../../misc/api';
+import { useGetScoreQuery, useGetSessionQuery } from '../../services/session';
+import { showModal } from '../../store/slices/screen';
+import { Skeleton } from '../../components/Skeleton';
-import './MisshaiPage.scss';
-import { DeveloperInfo } from './DeveloperInfo';
+import './misshai.scss';
+import { Ranking } from '../../components/Ranking';
+import { useTitle } from '../../hooks/useTitle';
const variables = [
'notesCount',
@@ -43,14 +42,15 @@ type DraftReducer = React.Reducer>;
export const MisshaiPage: React.VFC = () => {
const dispatch = useDispatch();
- const [limit, setLimit] = useState(10);
-
const session = useGetSessionQuery(undefined);
+ const [limit, setLimit] = useState(10);
const data = session.data;
const score = useGetScoreQuery(undefined);
const {t} = useTranslation();
+ useTitle('_sidebar.missHaiAlert');
+
const [draft, dispatchDraft] = useReducer((state, action) => {
return { ...state, ...action };
}, {
@@ -195,54 +195,42 @@ export const MisshaiPage: React.VFC = () => {
) : (
-
-
+
+
-
-
-
-
{t('_missHai.data')}
-
-
-
- |
- {t('_missHai.dataScore')} |
- {t('_missHai.dataDelta')} |
-
-
-
-
- {t('notes')} |
- {score.data.notesCount} |
- {score.data.notesDelta} |
-
-
- {t('following')} |
- {score.data.followingCount} |
- {score.data.followingDelta} |
-
-
- {t('followers')} |
- {score.data.followersCount} |
- {score.data.followersDelta} |
-
-
-
-
-
- {t('_missHai.rating')}{': '}
-
- {session.data.rating}
-
-
-
-
-
-
-
+
{t('_missHai.data')}
+
+
+
+ |
+ {t('_missHai.dataScore')} |
+ {t('_missHai.dataDelta')} |
+
+
+
+
+ {t('notes')} |
+ {score.data.notesCount} |
+ {score.data.notesDelta} |
+
+
+ {t('following')} |
+ {score.data.followingCount} |
+ {score.data.followingDelta} |
+
+
+ {t('followers')} |
+ {score.data.followersCount} |
+ {score.data.followersDelta} |
+
+
+
+
+
+ {t('_missHai.rating')}{': '}
+
+ {session.data.rating}
+
@@ -261,8 +249,8 @@ export const MisshaiPage: React.VFC = () => {
-
{t('alertMode')}
-
+
{t('alertMode')}
+
{ alertModes.map((mode) => (
{ (draft.alertMode === 'notification' || draft.alertMode === 'both') && (
-
+
{t('_alertMode.notificationWarning')}
)}
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
<>
{t('visibility')}
-
+
{
availableVisibilities.map((visibility) => (
-
{t('template')}
+
{t('template')}
{t('_template.description')}
-
+ {'{ } '}
{t('_template.insertVariables')}
-
+
-
+
{t('sendAlert')}
{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}
diff --git a/src/frontend/pages/ranking.tsx b/src/frontend/pages/apps/misshai/ranking.tsx
similarity index 73%
rename from src/frontend/pages/ranking.tsx
rename to src/frontend/pages/apps/misshai/ranking.tsx
index 19fed8e..ad77181 100644
--- a/src/frontend/pages/ranking.tsx
+++ b/src/frontend/pages/apps/misshai/ranking.tsx
@@ -1,10 +1,12 @@
import React from 'react';
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 = () => {
const {t} = useTranslation();
+ useTitle('_missHai.ranking');
return (
diff --git a/src/frontend/pages/index.session.tsx b/src/frontend/pages/index.session.tsx
index 18f4ac5..c72fcb3 100644
--- a/src/frontend/pages/index.session.tsx
+++ b/src/frontend/pages/index.session.tsx
@@ -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 { AccountsPage } from '../components/AccountsPage';
import { useDispatch } from 'react-redux';
import { LOCALSTORAGE_KEY_ACCOUNTS } from '../const';
import { IUser } from '../../common/types/user';
import { setAccounts } from '../store/slices/screen';
-import { useGetMetaQuery, useGetSessionQuery } from '../services/session';
-import { AdminPage } from '../components/AdminPage';
+import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { $get } from '../misc/api';
-import { NekomimiPage } from '../components/NekomimiPage';
-import { Card } from '../components/Card';
-import { CurrentUser } from '../components/CurrentUser';
+import { AnnouncementList } from '../components/AnnouncementList';
+import { DeveloperInfo } from '../components/DeveloperInfo';
export const IndexSessionPage: React.VFC = () => {
- const [selectedTab, setSelectedTab] = useState('misshai');
- const {t, i18n} = useTranslation();
+ const {t} = useTranslation();
const dispatch = useDispatch();
const { data: session } = useGetSessionQuery(undefined);
- const { data: meta } = useGetMetaQuery(undefined);
+ const score = useGetScoreQuery(undefined);
useEffect(() => {
const accounts = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[];
Promise.all(accounts.map(token => $get('session', token))).then(a => dispatch(setAccounts(a as IUser[])));
}, [dispatch]);
- const items = useMemo(() => {
- 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 ;
- case 'accounts': return ;
- case 'admin': return ;
- case 'nekomimi': return ;
- case 'settings': return ;
- default: return null;
- }
- }, [selectedTab]);
-
return (
- <>
-
-
-
-
+
+
-
-
-
- {session && meta && meta.currentTokenVersion !== session.tokenVersion && (
-
- )}
-
- {component}
+
+
+
+
{t('_missHai.data')}
+
+
+
+ |
+ {t('_missHai.dataScore')} |
+ {t('_missHai.dataDelta')} |
+
+
+
+
+ {t('notes')} |
+ {score.data?.notesCount ?? '...'} |
+ {score.data?.notesDelta ?? '...'} |
+
+
+ {t('following')} |
+ {score.data?.followingCount ?? '...'} |
+ {score.data?.followingDelta ?? '...'} |
+
+
+ {t('followers')} |
+ {score.data?.followersCount ?? '...'} |
+ {score.data?.followersDelta ?? '...'} |
+
+
+
+
+
+ {t('_missHai.rating')}{': '}
+
+ {session?.rating ?? '...'}
+
+
+
+
- >
+
);
};
diff --git a/src/frontend/pages/index.welcome.tsx b/src/frontend/pages/index.welcome.tsx
index e3a4a94..83a09f3 100644
--- a/src/frontend/pages/index.welcome.tsx
+++ b/src/frontend/pages/index.welcome.tsx
@@ -1,28 +1,122 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { LoginForm } from '../components/LoginForm';
-import { Header } from '../components/Header';
-import { AnnouncementList } from '../components/AnnouncementList';
+import styled from 'styled-components';
+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
`
+ 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 = () => {
+ const [announcements, setAnnouncements] = useState([]);
+
+ const {isMobile} = useSelector(state => state.screen);
const {t} = useTranslation();
+ const fetchAllAnnouncements = () => {
+ setAnnouncements([]);
+ $get('announcements').then(announcements => {
+ setAnnouncements(announcements ?? []);
+ });
+ };
+
+ useEffect(() => {
+ fetchAllAnnouncements();
+ }, []);
+
return (
<>
-
-
- {t('description1')}
- {t('description2')}
-
-
-
-
-
-
+
+
+
{t('title')}
+
{t('description1')}
+
{t('description2')}
+
+
+
-
-
+
+
お知らせ
+
+ {announcements.map(a => (
+
+ {a.title}
+
+ ))}
+
+
+
+
+
+
+ 👍 ❤ 😆 🎉 🍮
+
+
{t('_welcome.title')}
@@ -30,18 +124,18 @@ export const IndexWelcomePage: React.VFC = () => {
- {t('_welcome.misshaiAlertTitle')}
+ {t('_welcome.misshaiAlertTitle')}
{t('_welcome.misshaiAlertDescription')}
- {t('_missHai.ranking')}
+ {t('_missHai.ranking')}
{t('_welcome.misshaiRankingDescription')}
- {t('_missHai.showRanking')}
+ {t('_missHai.showRanking')}
-
-
{t('catAdjuster')}
+
+ {t('catAdjuster')}
{t('_welcome.catAdjusterDescription')}
-
+
{t('_welcome.nextFeaturesTitle')}
diff --git a/src/frontend/components/SettingPage.tsx b/src/frontend/pages/settings.tsx
similarity index 85%
rename from src/frontend/components/SettingPage.tsx
rename to src/frontend/pages/settings.tsx
index 4073078..311516b 100644
--- a/src/frontend/components/SettingPage.tsx
+++ b/src/frontend/pages/settings.tsx
@@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useGetSessionQuery } from '../services/session';
-import { Card } from './Card';
+import { Card } from '../components/Card';
import { Theme, themes } from '../misc/theme';
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { changeLang, changeTheme, showModal } from '../store/slices/screen';
import { useSelector } from '../store';
import { languageName } from '../langs';
import { $delete } from '../misc/api';
+import { useTitle } from '../hooks/useTitle';
export const SettingPage: React.VFC = () => {
const session = useGetSessionQuery(undefined);
@@ -18,6 +19,8 @@ export const SettingPage: React.VFC = () => {
const data = session.data;
const {t} = useTranslation();
+ useTitle('_sidebar.settings');
+
const currentTheme = useSelector(state => state.screen.theme);
const currentLang = useSelector(state => state.screen.language);
@@ -39,7 +42,7 @@ export const SettingPage: React.VFC = () => {
onSelect(i) {
if (i === 0) {
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
- location.reload();
+ location.href = '/';
}
},
}));
@@ -69,7 +72,7 @@ export const SettingPage: React.VFC = () => {
message: t('_deactivate.success'),
icon: 'info',
onSelect() {
- location.reload();
+ location.href = '/';
}
}));
}).catch((e) => {
@@ -91,7 +94,7 @@ export const SettingPage: React.VFC = () => {
) : (
- {t('appearance')}
+ {t('appearance')}
{t('theme')}
{
@@ -115,21 +118,23 @@ export const SettingPage: React.VFC = () => {
}
-
+
{t('logout')}
{t('logoutDescription')}
-
+
{t('deleteAccount')}
{t('deleteAccountDescription')}
diff --git a/src/frontend/pages/term.tsx b/src/frontend/pages/term.tsx
deleted file mode 100644
index ddeec9b..0000000
--- a/src/frontend/pages/term.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-
-export const TermPage: React.VFC = () => {
- // TODO: 外部サイトに誘導する
- return (
-
- 利用規約
-
- -
- 本サービスは「現状のまま」「無保証」で提供されます。本サービスを利用したことによる損害など
- について、管理人は一切責任を負わないものとします。
-
- -
- 本サービスは、Misskey プロジェクトとは無関係です。
- 本サービスに関して Misskey プロジェクトに問い合わせる行為はお控え下さい。
-
- -
- ユーザーはインスタンスの諸規約に従った上で本サービスを使うものとします。
- インスタンスの規約により自動投稿が禁止されている場合は本サービスを使用しないでください。
-
- -
- 本サービスでは、接続先のアカウントが存在しない、トークンが失効してしまったなどの場合に、
- 自動的にユーザーアカウントを削除します。
-
- -
- 運営は、正当な理由がある場合に限り、本サービスを特定のインスタンスで利用できないよう
- ブロックする可能性があります。
-
- - 本サービスの仕様は、事前の予告無しに変更される可能性があります。
- - 本サービスは、事前の予告無しに突然閉鎖される可能性があります。
- - 本規約は、事前の予告無しに変更される可能性があります。
-
-
- );
-};
diff --git a/src/frontend/store/slices/screen.ts b/src/frontend/store/slices/screen.ts
index ed2db3d..0a45ec9 100644
--- a/src/frontend/store/slices/screen.ts
+++ b/src/frontend/store/slices/screen.ts
@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import i18n from 'i18next';
+import { WritableDraft } from 'immer/dist/internal';
import { IUser } from '../../../common/types/user';
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
@@ -10,9 +11,12 @@ interface ScreenState {
modal: Modal | null;
modalShown: boolean;
theme: Theme;
+ title: string | null;
language: string;
accounts: IUser[];
accountTokens: string[];
+ isMobile: boolean;
+ isDrawerShown: boolean;
}
const initialState: ScreenState = {
@@ -20,8 +24,21 @@ const initialState: ScreenState = {
modalShown: false,
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
+ title: null,
accounts: [],
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
+ isMobile: false,
+ isDrawerShown: false,
+};
+
+/**
+ * 値を設定するReducerを生成します。
+ */
+const generateSetter =
>(key: T, callback?: (state: WritableDraft, action: PayloadAction) => void) => {
+ return (state: WritableDraft, action: PayloadAction) => {
+ state[key] = action.payload;
+ if (callback) callback(state, action);
+ };
};
export const screenSlice = createSlice({
@@ -36,23 +53,23 @@ export const screenSlice = createSlice({
state.modal = null;
state.modalShown = false;
},
- changeTheme: (state, action: PayloadAction) => {
- state.theme = action.payload;
+ changeTheme: generateSetter('theme', (_, action) => {
localStorage[LOCALSTORAGE_KEY_THEME] = action.payload;
- },
- changeLang: (state, action: PayloadAction) => {
- state.language = action.payload;
+ }),
+ changeLang: generateSetter('language', (_, action) => {
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
i18n.changeLanguage(action.payload);
- },
- setAccounts: (state, action: PayloadAction) => {
- state.accounts = action.payload;
+ }),
+ setAccounts: generateSetter('accounts', (state, action) => {
state.accountTokens = action.payload.map(a => a.misshaiToken);
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;
diff --git a/src/frontend/style.scss b/src/frontend/style.scss
index 9261319..08abc35 100644
--- a/src/frontend/style.scss
+++ b/src/frontend/style.scss
@@ -19,6 +19,15 @@ hr {
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 {
animation: 0.3s ease-out 0s fadeIn;
&.down {
@@ -94,6 +103,10 @@ small {
max-width: min(100vw, 600px);
}
+.card > .body > h1 {
+ margin-bottom: var(--margin);
+}
+
.modal-menu-wrapper {
display: flex;
position: fixed;
@@ -130,4 +143,10 @@ small {
animation: earwiggleright 1s infinite;
}
}
+}
+
+.twemoji {
+ height: 1em;
+ width: 1em;
+ vertical-align: -0.1em;
}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 07c0773..61560c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -355,6 +355,13 @@
dependencies:
"@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":
version "3.7.1"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
@@ -615,6 +622,13 @@
"@types/history" "*"
"@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":
version "17.0.27"
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"
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:
version "1.1.3"
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"
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:
version "1.0.0"
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"
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:
version "2.0.0"
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:
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:
version "1.0.0"
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"
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:
version "4.6.2"
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-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:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -5750,6 +5813,21 @@ tsutils@^3.21.0:
dependencies:
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:
version "0.4.0"
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-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:
version "1.0.0"
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==
xeltica-ui@xeltica/design-system:
- version "1.0.0-beta.7"
- resolved "https://codeload.github.com/xeltica/design-system/tar.gz/d0b49b6487c379a10f6e568c0f9e37b0035ea7f2"
+ version "1.0.0-beta.9"
+ resolved "https://codeload.github.com/xeltica/design-system/tar.gz/83b7faede9b0a42a9a3bd8d462d95484cfa67294"
xml2js@^0.4.17:
version "0.4.23"