UI調整など
This commit is contained in:
parent
e3178f30aa
commit
7d40708c42
17 changed files with 359 additions and 316 deletions
18
src/common/types/design-system-color.ts
Normal file
18
src/common/types/design-system-color.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export const designSystemColors = [
|
||||||
|
'red',
|
||||||
|
'vermilion',
|
||||||
|
'orange',
|
||||||
|
'yellow',
|
||||||
|
'lime',
|
||||||
|
'green',
|
||||||
|
'teal',
|
||||||
|
'cyan',
|
||||||
|
'skyblue',
|
||||||
|
'blue',
|
||||||
|
'indigo',
|
||||||
|
'purple',
|
||||||
|
'magenta',
|
||||||
|
'pink',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type DesignSystemColor = typeof designSystemColors[number];
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { IAnnouncement } from '../../common/types/announcement';
|
import { IAnnouncement } from '../../common/types/announcement';
|
||||||
|
@ -7,7 +6,6 @@ import { $get } from '../misc/api';
|
||||||
|
|
||||||
export const AnnouncementList: React.VFC = () => {
|
export const AnnouncementList: React.VFC = () => {
|
||||||
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||||
const {t} = useTranslation();
|
|
||||||
|
|
||||||
const fetchAllAnnouncements = () => {
|
const fetchAllAnnouncements = () => {
|
||||||
setAnnouncements([]);
|
setAnnouncements([]);
|
||||||
|
@ -24,8 +22,7 @@ export const AnnouncementList: React.VFC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="mb-0"><i className="fas fa-bell"></i> {t('announcements')}</h1>
|
<div className="large menu xmenu 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}`}>
|
||||||
{a.title}
|
{a.title}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export const DeveloperInfo: React.VFC = () => {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1><i className="fas fa-circle-question"></i> {t('_developerInfo.title')}</h1>
|
|
||||||
<p>{t('_developerInfo.description')}</p>
|
|
||||||
<div className="menu large">
|
|
||||||
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
|
|
||||||
<i className="icon fas fa-at"></i>
|
|
||||||
Lutica@groundpolis.app
|
|
||||||
</a>
|
|
||||||
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
|
|
||||||
<i className="icon fas fa-at"></i>
|
|
||||||
le@misskey.io
|
|
||||||
</a>
|
|
||||||
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
|
|
||||||
<i className="icon fas fa-at"></i>
|
|
||||||
EbiseLutica@twitter.com
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -45,9 +45,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<div className="alert bg-danger">{t('failedToFetch')}</div>
|
<div className="alert bg-danger">{t('failedToFetch')}</div>
|
||||||
) : response ? (
|
) : response ? (
|
||||||
<>
|
response.isCalculating ? (
|
||||||
<aside>{t('registeredUsersCount')}: {response?.userCount}</aside>
|
|
||||||
{response.isCalculating ? (
|
|
||||||
<p>{t('isCalculating')}</p>
|
<p>{t('isCalculating')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="menu large">
|
<div className="menu large">
|
||||||
|
@ -61,28 +59,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
// <table className="table mt-1 fluid">
|
)
|
||||||
// <thead>
|
|
||||||
// <tr>
|
|
||||||
// <th>{t('_missHai.order')}</th>
|
|
||||||
// <th>{t('name')}</th>
|
|
||||||
// <th>{t('_missHai.rating')}</th>
|
|
||||||
// </tr>
|
|
||||||
// </thead>
|
|
||||||
// <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,11 +1,20 @@
|
||||||
|
/** ローカルストレージキー Misskey Tools API トークン */
|
||||||
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
||||||
|
/** ローカルストレージキー テーマ設定 */
|
||||||
export const LOCALSTORAGE_KEY_THEME = 'theme';
|
export const LOCALSTORAGE_KEY_THEME = 'theme';
|
||||||
|
/** ローカルストレージキー 言語設定 */
|
||||||
export const LOCALSTORAGE_KEY_LANG = 'lang';
|
export const LOCALSTORAGE_KEY_LANG = 'lang';
|
||||||
|
/** ローカルストレージキー ログイン済みアカウント一覧 */
|
||||||
export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
|
export const LOCALSTORAGE_KEY_ACCOUNTS = 'accounts';
|
||||||
|
/** ローカルストレージキー アクセントカラー設定 */
|
||||||
|
export const LOCALSTORAGE_KEY_ACCENT_COLOR = 'accent_color';
|
||||||
|
|
||||||
|
/** Misskey Tools API エンドポイント */
|
||||||
export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
||||||
|
/** 更新履歴URL */
|
||||||
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';
|
||||||
|
/** Xeltica Studio 公式サイトURL */
|
||||||
export const XELTICA_STUDIO_URL = 'https://xeltica.work';
|
export const XELTICA_STUDIO_URL = 'https://xeltica.work';
|
||||||
|
|
||||||
|
/** ブレークポイント モバイル */
|
||||||
export const BREAKPOINT_SM = '800px';
|
export const BREAKPOINT_SM = '800px';
|
||||||
|
|
20
src/frontend/hooks/useAnnouncements.ts
Normal file
20
src/frontend/hooks/useAnnouncements.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { IAnnouncement } from '../../common/types/announcement';
|
||||||
|
import { $get } from '../misc/api';
|
||||||
|
|
||||||
|
export const useAnnouncements = () => {
|
||||||
|
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||||
|
|
||||||
|
const fetchAllAnnouncements = () => {
|
||||||
|
setAnnouncements([]);
|
||||||
|
$get<IAnnouncement[]>('announcements').then(announcements => {
|
||||||
|
setAnnouncements(announcements ?? []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllAnnouncements();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return announcements;
|
||||||
|
};
|
|
@ -50,6 +50,8 @@
|
||||||
"shareMisskeyTools": "#MisskeyTools をシェアする",
|
"shareMisskeyTools": "#MisskeyTools をシェアする",
|
||||||
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
|
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
|
||||||
"instanceUrlPlaceholder": "例:misskey.io",
|
"instanceUrlPlaceholder": "例:misskey.io",
|
||||||
|
"settings": "設定",
|
||||||
|
"accentColor": "アクセントカラー",
|
||||||
"_sidebar": {
|
"_sidebar": {
|
||||||
"dashboard": "ダッシュボード",
|
"dashboard": "ダッシュボード",
|
||||||
"tools": "ツール",
|
"tools": "ツール",
|
||||||
|
@ -100,7 +102,6 @@
|
||||||
"useRanking": "ランキングに参加する"
|
"useRanking": "ランキングに参加する"
|
||||||
},
|
},
|
||||||
"_accounts": {
|
"_accounts": {
|
||||||
"currentAccount": "現在ログインしているアカウント",
|
|
||||||
"switchAccount": "アカウント切り替え",
|
"switchAccount": "アカウント切り替え",
|
||||||
"useAnother": "他のアカウントで登録する"
|
"useAnother": "他のアカウントで登録する"
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type Theme = typeof themes[number];
|
||||||
export type ActualTheme = typeof actualThemes[number];
|
export type ActualTheme = typeof actualThemes[number];
|
||||||
|
|
||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
const theme = useSelector(state => state.screen.theme);
|
const {theme, accentColor} = useSelector(state => state.screen);
|
||||||
|
|
||||||
const [ osTheme, setOsTheme ] = useState<ActualTheme>('dark');
|
const [ osTheme, setOsTheme ] = useState<ActualTheme>('dark');
|
||||||
|
|
||||||
|
@ -45,4 +45,13 @@ export const useTheme = () => {
|
||||||
q.removeEventListener('change', listener);
|
q.removeEventListener('change', listener);
|
||||||
};
|
};
|
||||||
}, [osTheme, setOsTheme]);
|
}, [osTheme, setOsTheme]);
|
||||||
|
|
||||||
|
// カラー変更に追従する
|
||||||
|
useEffect(() => {
|
||||||
|
const {style} = document.body;
|
||||||
|
style.setProperty('--primary', `var(--${accentColor})`);
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
style.setProperty(`--primary-${i}`, `var(--${accentColor}-${i})`);
|
||||||
|
}
|
||||||
|
}, [accentColor]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,12 +33,11 @@ export const AccountsPage: React.VFC = () => {
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<article className="card fade">
|
<article className="fade">
|
||||||
<div className="body">
|
<section>
|
||||||
<div>
|
<h2>{t('_accounts.switchAccount')}</h2>
|
||||||
<strong>{t('_accounts.switchAccount')}</strong>
|
|
||||||
</div>
|
<div className="menu xmenu large fluid mb-2">
|
||||||
<div className="menu large fluid mb-2">
|
|
||||||
{
|
{
|
||||||
accounts.length === accountTokens.length ? (
|
accounts.length === accountTokens.length ? (
|
||||||
accounts.map(account => (
|
accounts.map(account => (
|
||||||
|
@ -59,8 +58,11 @@ export const AccountsPage: React.VFC = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>{t('_accounts.useAnother')}</h2>
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,10 +21,8 @@ export const AnnouncementPage: React.VFC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
|
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
|
||||||
}, [setAnnouncement]);
|
}, [setAnnouncement]);
|
||||||
return (
|
return !announcement ? <Skeleton width="100%" height="10rem" /> : (
|
||||||
<article>
|
<article className="fade">
|
||||||
{!announcement ? <Skeleton width="100%" height="10rem" /> : (
|
|
||||||
<>
|
|
||||||
<h2>
|
<h2>
|
||||||
{announcement.title}
|
{announcement.title}
|
||||||
<aside className="inline ml-1 text-dimmed text-100">
|
<aside className="inline ml-1 text-dimmed text-100">
|
||||||
|
@ -35,8 +33,6 @@ export const AnnouncementPage: React.VFC = () => {
|
||||||
<section>
|
<section>
|
||||||
<ReactMarkdown>{announcement.body}</ReactMarkdown>
|
<ReactMarkdown>{announcement.body}</ReactMarkdown>
|
||||||
</section>
|
</section>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useReducer, useRef } 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';
|
||||||
|
@ -44,7 +44,6 @@ type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
|
||||||
export const MisshaiPage: React.VFC = () => {
|
export const MisshaiPage: React.VFC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
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);
|
||||||
|
|
||||||
|
@ -196,10 +195,9 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
<Skeleton width="100%" height="160px" />
|
<Skeleton width="100%" height="160px" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="vstack fade">
|
<article className="fade">
|
||||||
<div className="card misshaiData">
|
<section className="misshaiData">
|
||||||
<div className="body">
|
<h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
|
||||||
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
|
||||||
<table className="table fluid">
|
<table className="table fluid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -226,31 +224,22 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>
|
<p><strong>{t('_missHai.rating')}{': '}</strong>{session.data.rating}</p>
|
||||||
<strong>
|
</section>
|
||||||
{t('_missHai.rating')}{': '}
|
<section className="misshaiRanking">
|
||||||
</strong>
|
<h2><i className="fas fa-ranking-star"/> {t('_missHai.ranking')}</h2>
|
||||||
{session.data.rating}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card misshaiRanking">
|
|
||||||
<div className="body">
|
|
||||||
<h1><i className="bi bi-bar-chart"></i> {t('_missHai.ranking')}</h1>
|
|
||||||
<Ranking limit={10} />
|
<Ranking limit={10} />
|
||||||
<Link to="/apps/miss-hai/ranking" className="btn primary" onClick={() => setLimit(undefined)}>{t('_missHai.showAll')}</Link>
|
<Link to="/apps/miss-hai/ranking" className="block mt-2">{t('_missHai.showAll')}</Link>
|
||||||
|
</section>
|
||||||
|
<section className="alertModeSetting">
|
||||||
|
<h2><i className="fas fa-gear"></i> {t('settings')}</h2>
|
||||||
<label className="input-check mt-2">
|
<label className="input-check mt-2">
|
||||||
<input type="checkbox" checked={draft.useRanking} onChange={(e) => {
|
<input type="checkbox" checked={draft.useRanking} onChange={(e) => {
|
||||||
updateSetting({ useRanking: e.target.checked });
|
updateSetting({ useRanking: e.target.checked });
|
||||||
}}/>
|
}}/>
|
||||||
<span>{t('_missHai.useRanking')}</span>
|
<span>{t('_missHai.useRanking')}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<h3>{t('alertMode')}</h3>
|
||||||
</div>
|
|
||||||
<div className="misshaiPageLayout">
|
|
||||||
<div className="card alertModeSetting">
|
|
||||||
<div className="body">
|
|
||||||
<h1><i className="fas fa-gear"></i> {t('alertMode')}</h1>
|
|
||||||
<div className="vstack slim">
|
<div className="vstack slim">
|
||||||
{ alertModes.map((mode) => (
|
{ alertModes.map((mode) => (
|
||||||
<label key={mode} className="input-check">
|
<label key={mode} className="input-check">
|
||||||
|
@ -269,7 +258,7 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
)}
|
)}
|
||||||
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
||||||
<>
|
<>
|
||||||
<h2 className="mt-2 mb-1">{t('visibility')}</h2>
|
<h3>{t('visibility')}</h3>
|
||||||
<div className="vstack slim">
|
<div className="vstack slim">
|
||||||
{
|
{
|
||||||
availableVisibilities.map((visibility) => (
|
availableVisibilities.map((visibility) => (
|
||||||
|
@ -290,11 +279,7 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
<h3>{t('template')}</h3>
|
||||||
</div>
|
|
||||||
<div className="card templateSetting">
|
|
||||||
<div className="body">
|
|
||||||
<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}>
|
||||||
|
@ -309,16 +294,14 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
dispatchDraft({ template: e.target.value });
|
dispatchDraft({ template: e.target.value });
|
||||||
}} />
|
}} />
|
||||||
<small className="text-dimmed">{t('_template.description2')}</small>
|
<small className="text-dimmed">{t('_template.description2')}</small>
|
||||||
<div className="hstack mt-2" style={{justifyContent: 'flex-end'}}>
|
<div className="hstack mt-2">
|
||||||
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
|
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
|
||||||
<button className="btn primary" onClick={() => {
|
<button className="btn primary" onClick={() => {
|
||||||
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
|
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
|
||||||
}}>{t('save')}</button>
|
}}>{t('save')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
<section className="list-form mt-2">
|
||||||
</div>
|
|
||||||
<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 fas fa-paper-plane" />
|
<i className="icon fas fa-paper-plane" />
|
||||||
<div className="body">
|
<div className="body">
|
||||||
|
@ -326,8 +309,8 @@ export const MisshaiPage: React.VFC = () => {
|
||||||
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
|
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const RankingPage: React.VFC = () => {
|
||||||
useTitle('_missHai.ranking');
|
useTitle('_missHai.ranking');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="xarticle">
|
<article>
|
||||||
<h2>{t('_missHai.ranking')}</h2>
|
<h2>{t('_missHai.ranking')}</h2>
|
||||||
<section>
|
<section>
|
||||||
<p>{t('_missHai.rankingDescription')}</p>
|
<p>{t('_missHai.rankingDescription')}</p>
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { IUser } from '../../common/types/user';
|
||||||
import { setAccounts } from '../store/slices/screen';
|
import { setAccounts } from '../store/slices/screen';
|
||||||
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
||||||
import { $get } from '../misc/api';
|
import { $get } from '../misc/api';
|
||||||
import { AnnouncementList } from '../components/AnnouncementList';
|
import { useAnnouncements } from '../hooks/useAnnouncements';
|
||||||
import { DeveloperInfo } from '../components/DeveloperInfo';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export const IndexSessionPage: React.VFC = () => {
|
export const IndexSessionPage: React.VFC = () => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
@ -16,22 +16,28 @@ export const IndexSessionPage: React.VFC = () => {
|
||||||
const { data: session } = useGetSessionQuery(undefined);
|
const { data: session } = useGetSessionQuery(undefined);
|
||||||
const score = useGetScoreQuery(undefined);
|
const score = useGetScoreQuery(undefined);
|
||||||
|
|
||||||
|
const announcements = useAnnouncements();
|
||||||
|
|
||||||
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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vstack fade">
|
<article className="fade">
|
||||||
<div className="card announcement">
|
<section>
|
||||||
<div className="body">
|
<h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
|
||||||
<AnnouncementList />
|
<div className="large menu xmenu fade">
|
||||||
</div>
|
{announcements.map(a => (
|
||||||
|
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
||||||
|
{a.title}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<div className="misshaiPageLayout">
|
<div className="misshaiPageLayout">
|
||||||
<div className="card misshaiData">
|
<section className="misshaiData">
|
||||||
<div className="body">
|
<h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
|
||||||
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
|
||||||
<table className="table fluid">
|
<table className="table fluid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -64,14 +70,26 @@ export const IndexSessionPage: React.VFC = () => {
|
||||||
</strong>
|
</strong>
|
||||||
{session?.rating ?? '...'}
|
{session?.rating ?? '...'}
|
||||||
</p>
|
</p>
|
||||||
|
</section>
|
||||||
|
<section className="developerInfo">
|
||||||
|
<h2><i className="fas fa-circle-question"></i> {t('_developerInfo.title')}</h2>
|
||||||
|
<p>{t('_developerInfo.description')}</p>
|
||||||
|
<div className="menu large">
|
||||||
|
<a className="item" href="http://groundpolis.app/@Lutica" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i className="icon fas fa-at"></i>
|
||||||
|
Lutica@groundpolis.app
|
||||||
|
</a>
|
||||||
|
<a className="item" href="http://misskey.io/@le" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i className="icon fas fa-at"></i>
|
||||||
|
le@misskey.io
|
||||||
|
</a>
|
||||||
|
<a className="item" href="http://twitter.com/@EbiseLutica" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i className="icon fas fa-at"></i>
|
||||||
|
EbiseLutica@twitter.com
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div className="card developerInfo">
|
</article>
|
||||||
<div className="body">
|
|
||||||
<DeveloperInfo />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -6,9 +6,8 @@ import { LoginForm } from '../components/LoginForm';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useSelector } from '../store';
|
import { useSelector } from '../store';
|
||||||
import { IsMobileProp } from '../misc/is-mobile-prop';
|
import { IsMobileProp } from '../misc/is-mobile-prop';
|
||||||
import { IAnnouncement } from '../../common/types/announcement';
|
|
||||||
import { $get } from '../misc/api';
|
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
|
import { useAnnouncements } from '../hooks/useAnnouncements';
|
||||||
|
|
||||||
const Hero = styled.div<IsMobileProp>`
|
const Hero = styled.div<IsMobileProp>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -68,21 +67,10 @@ const FormWrapper = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const IndexWelcomePage: React.VFC = () => {
|
export const IndexWelcomePage: React.VFC = () => {
|
||||||
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
|
||||||
|
|
||||||
const {isMobile} = useSelector(state => state.screen);
|
const {isMobile} = useSelector(state => state.screen);
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
|
|
||||||
const fetchAllAnnouncements = () => {
|
const announcements = useAnnouncements();
|
||||||
setAnnouncements([]);
|
|
||||||
$get<IAnnouncement[]>('announcements').then(announcements => {
|
|
||||||
setAnnouncements(announcements ?? []);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchAllAnnouncements();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -96,8 +84,8 @@ export const IndexWelcomePage: React.VFC = () => {
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div className="announcements">
|
<div className="announcements">
|
||||||
<h2>お知らせ</h2>
|
<h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
|
||||||
<div className="menu large">
|
<div className="menu xmenu">
|
||||||
{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}`}>
|
||||||
{a.title}
|
{a.title}
|
||||||
|
|
|
@ -6,11 +6,33 @@ import { useGetSessionQuery } from '../services/session';
|
||||||
import { Card } from '../components/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 { changeAccentColor, 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';
|
import { useTitle } from '../hooks/useTitle';
|
||||||
|
import { designSystemColors } from '../../common/types/design-system-color';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const ColorInput = styled.input<{color: string}>`
|
||||||
|
display: block;
|
||||||
|
appearance: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--panel);
|
||||||
|
border: 4px solid var(--${p => p.color});
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
&:checked {
|
||||||
|
background: var(--${p => p.color});
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
&:hover, &:focus {
|
||||||
|
box-shadow: 0 0 16px var(--${p => p.color});
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingPage: React.VFC = () => {
|
export const SettingPage: React.VFC = () => {
|
||||||
const session = useGetSessionQuery(undefined);
|
const session = useGetSessionQuery(undefined);
|
||||||
|
@ -23,6 +45,7 @@ export const SettingPage: React.VFC = () => {
|
||||||
|
|
||||||
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);
|
||||||
|
const currentAccentColor = useSelector(state => state.screen.accentColor);
|
||||||
|
|
||||||
const onClickLogout = useCallback(() => {
|
const onClickLogout = useCallback(() => {
|
||||||
dispatch(showModal({
|
dispatch(showModal({
|
||||||
|
@ -92,10 +115,10 @@ export const SettingPage: React.VFC = () => {
|
||||||
return session.isLoading || !data ? (
|
return session.isLoading || !data ? (
|
||||||
<div className="skeleton" style={{width: '100%', height: '128px'}}></div>
|
<div className="skeleton" style={{width: '100%', height: '128px'}}></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="vstack fade">
|
<article className="fade">
|
||||||
<Card bodyClassName="vstack">
|
<h2><i className="fas fa-palette"></i> {t('appearance')}</h2>
|
||||||
<h1><i className="fas fa-palette"></i> {t('appearance')}</h1>
|
<section>
|
||||||
<h2>{t('theme')}</h2>
|
<h3>{t('theme')}</h3>
|
||||||
<div className="vstack">
|
<div className="vstack">
|
||||||
{
|
{
|
||||||
themes.map(theme => (
|
themes.map(theme => (
|
||||||
|
@ -106,11 +129,19 @@ export const SettingPage: React.VFC = () => {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<h2>{t('language')}</h2>
|
<section>
|
||||||
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => {
|
<h3>{t('accentColor')}</h3>
|
||||||
dispatch(changeLang(e.target.value));
|
<div className="hstack slim wrap mb-2">
|
||||||
}}>
|
{designSystemColors.map(c => (
|
||||||
|
<ColorInput className="shadow-2" type="radio" color={c} value={c} checked={c === currentAccentColor} onChange={e => dispatch(changeAccentColor(e.target.value))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="btn primary">{t('resetToDefault')}</button>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>{t('language')}</h3>
|
||||||
|
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => dispatch(changeLang(e.target.value))}>
|
||||||
{
|
{
|
||||||
(Object.keys(languageName) as Array<keyof typeof languageName>).map(n => (
|
(Object.keys(languageName) as Array<keyof typeof languageName>).map(n => (
|
||||||
<option value={n} key={n}>{languageName[n]}</option>
|
<option value={n} key={n}>{languageName[n]}</option>
|
||||||
|
@ -124,7 +155,9 @@ export const SettingPage: React.VFC = () => {
|
||||||
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
|
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>その他の設定</h2>
|
||||||
<div className="list-form">
|
<div className="list-form">
|
||||||
<button className="item" onClick={onClickLogout}>
|
<button className="item" onClick={onClickLogout}>
|
||||||
<i className="icon fas fa-arrow-up-right-from-square" />
|
<i className="icon fas fa-arrow-up-right-from-square" />
|
||||||
|
@ -141,6 +174,7 @@ export const SettingPage: React.VFC = () => {
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
</article>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
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 { WritableDraft } from 'immer/dist/internal';
|
||||||
import { IUser } from '../../../common/types/user';
|
|
||||||
|
|
||||||
import { LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
import { LOCALSTORAGE_KEY_ACCENT_COLOR, LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
||||||
import { Theme } from '../../misc/theme';
|
import { Theme } from '../../misc/theme';
|
||||||
import { Modal } from '../../modal/modal';
|
import { Modal } from '../../modal/modal';
|
||||||
|
import { IUser } from '../../../common/types/user';
|
||||||
|
import { DesignSystemColor } from '../../../common/types/design-system-color';
|
||||||
|
|
||||||
interface ScreenState {
|
interface ScreenState {
|
||||||
modal: Modal | null;
|
modal: Modal | null;
|
||||||
|
@ -13,6 +14,7 @@ interface ScreenState {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
title: string | null;
|
title: string | null;
|
||||||
language: string;
|
language: string;
|
||||||
|
accentColor: DesignSystemColor;
|
||||||
accounts: IUser[];
|
accounts: IUser[];
|
||||||
accountTokens: string[];
|
accountTokens: string[];
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
@ -24,6 +26,7 @@ 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',
|
||||||
|
accentColor: localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] ?? 'green',
|
||||||
title: null,
|
title: null,
|
||||||
accounts: [],
|
accounts: [],
|
||||||
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
||||||
|
@ -60,6 +63,9 @@ export const screenSlice = createSlice({
|
||||||
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
||||||
i18n.changeLanguage(action.payload);
|
i18n.changeLanguage(action.payload);
|
||||||
}),
|
}),
|
||||||
|
changeAccentColor: generateSetter('accentColor', (_, action) => {
|
||||||
|
localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] = action.payload;
|
||||||
|
}),
|
||||||
setAccounts: generateSetter('accounts', (state, action) => {
|
setAccounts: generateSetter('accounts', (state, action) => {
|
||||||
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);
|
||||||
|
@ -70,6 +76,6 @@ export const screenSlice = createSlice({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { showModal, hideModal, changeTheme, changeLang, setAccounts, setMobile, setTitle, setDrawerShown } = screenSlice.actions;
|
export const { showModal, hideModal, changeTheme, changeLang, changeAccentColor, setAccounts, setMobile, setTitle, setDrawerShown } = screenSlice.actions;
|
||||||
|
|
||||||
export default screenSlice.reducer;
|
export default screenSlice.reducer;
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
body {
|
body {
|
||||||
--primary: rgb(134, 179, 0);
|
|
||||||
--primary-d: rgb(52, 70, 0);
|
|
||||||
--max-width: 1024px;
|
--max-width: 1024px;
|
||||||
font-family: "Koruri", sans-serif;
|
font-family: "Koruri", sans-serif;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +17,19 @@ hr {
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xmenu {
|
||||||
|
.item {
|
||||||
|
background: #ffffff40;
|
||||||
|
+ .item {
|
||||||
|
margin-top: var(--slim-margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .xmenu .item {
|
||||||
|
background: #00000040;
|
||||||
|
}
|
||||||
|
|
||||||
._header {
|
._header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--container-padding);
|
top: var(--container-padding);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue