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 { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { IAnnouncement } from '../../common/types/announcement';
|
||||
|
@ -7,7 +6,6 @@ import { $get } from '../misc/api';
|
|||
|
||||
export const AnnouncementList: React.VFC = () => {
|
||||
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const fetchAllAnnouncements = () => {
|
||||
setAnnouncements([]);
|
||||
|
@ -24,8 +22,7 @@ export const AnnouncementList: React.VFC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-0"><i className="fas fa-bell"></i> {t('announcements')}</h1>
|
||||
<div className="large menu fade">
|
||||
<div className="large menu xmenu fade">
|
||||
{announcements.map(a => (
|
||||
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
||||
{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,44 +45,21 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
|
|||
) : isError ? (
|
||||
<div className="alert bg-danger">{t('failedToFetch')}</div>
|
||||
) : response ? (
|
||||
<>
|
||||
<aside>{t('registeredUsersCount')}: {response?.userCount}</aside>
|
||||
{response.isCalculating ? (
|
||||
<p>{t('isCalculating')}</p>
|
||||
) : (
|
||||
<div className="menu large">
|
||||
{response.ranking.map((r, i) => (
|
||||
<a href={`https://${r.host}/@${r.username}`} target="_blank" rel="noopener noreferrer nofollow" className="item flex" key={i}>
|
||||
<div className="text-bold pr-2">{i + 1}</div>
|
||||
<div>
|
||||
{r.username}@{r.host}<br/>
|
||||
<span className="text-dimmed text-75">{t('_missHai.rating')}: {r.rating}</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
response.isCalculating ? (
|
||||
<p>{t('isCalculating')}</p>
|
||||
) : (
|
||||
<div className="menu large">
|
||||
{response.ranking.map((r, i) => (
|
||||
<a href={`https://${r.host}/@${r.username}`} target="_blank" rel="noopener noreferrer nofollow" className="item flex" key={i}>
|
||||
<div className="text-bold pr-2">{i + 1}</div>
|
||||
<div>
|
||||
{r.username}@{r.host}<br/>
|
||||
<span className="text-dimmed text-75">{t('_missHai.rating')}: {r.rating}</span>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : null
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
/** ローカルストレージキー Misskey Tools API トークン */
|
||||
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
||||
/** ローカルストレージキー テーマ設定 */
|
||||
export const LOCALSTORAGE_KEY_THEME = 'theme';
|
||||
/** ローカルストレージキー 言語設定 */
|
||||
export const LOCALSTORAGE_KEY_LANG = 'lang';
|
||||
/** ローカルストレージキー ログイン済みアカウント一覧 */
|
||||
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/`;
|
||||
|
||||
/** 更新履歴URL */
|
||||
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 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 をシェアする",
|
||||
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://misskey.tools",
|
||||
"instanceUrlPlaceholder": "例:misskey.io",
|
||||
"settings": "設定",
|
||||
"accentColor": "アクセントカラー",
|
||||
"_sidebar": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"tools": "ツール",
|
||||
|
@ -100,7 +102,6 @@
|
|||
"useRanking": "ランキングに参加する"
|
||||
},
|
||||
"_accounts": {
|
||||
"currentAccount": "現在ログインしているアカウント",
|
||||
"switchAccount": "アカウント切り替え",
|
||||
"useAnother": "他のアカウントで登録する"
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ export type Theme = typeof themes[number];
|
|||
export type ActualTheme = typeof actualThemes[number];
|
||||
|
||||
export const useTheme = () => {
|
||||
const theme = useSelector(state => state.screen.theme);
|
||||
const {theme, accentColor} = useSelector(state => state.screen);
|
||||
|
||||
const [ osTheme, setOsTheme ] = useState<ActualTheme>('dark');
|
||||
|
||||
|
@ -45,4 +45,13 @@ export const useTheme = () => {
|
|||
q.removeEventListener('change', listener);
|
||||
};
|
||||
}, [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 />
|
||||
</div>
|
||||
) : (
|
||||
<article className="card fade">
|
||||
<div className="body">
|
||||
<div>
|
||||
<strong>{t('_accounts.switchAccount')}</strong>
|
||||
</div>
|
||||
<div className="menu large fluid mb-2">
|
||||
<article className="fade">
|
||||
<section>
|
||||
<h2>{t('_accounts.switchAccount')}</h2>
|
||||
|
||||
<div className="menu xmenu large fluid mb-2">
|
||||
{
|
||||
accounts.length === accountTokens.length ? (
|
||||
accounts.map(account => (
|
||||
|
@ -59,8 +58,11 @@ export const AccountsPage: React.VFC = () => {
|
|||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>{t('_accounts.useAnother')}</h2>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,22 +21,18 @@ export const AnnouncementPage: React.VFC = () => {
|
|||
useEffect(() => {
|
||||
$get<IAnnouncement>('announcements/' + id).then(setAnnouncement);
|
||||
}, [setAnnouncement]);
|
||||
return (
|
||||
<article>
|
||||
{!announcement ? <Skeleton width="100%" height="10rem" /> : (
|
||||
<>
|
||||
<h2>
|
||||
{announcement.title}
|
||||
<aside className="inline ml-1 text-dimmed text-100">
|
||||
<i className="fas fa-clock" />
|
||||
{dayjs(announcement.createdAt).locale(lang.split('_')[0]).fromNow()}
|
||||
</aside>
|
||||
</h2>
|
||||
<section>
|
||||
<ReactMarkdown>{announcement.body}</ReactMarkdown>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
return !announcement ? <Skeleton width="100%" height="10rem" /> : (
|
||||
<article className="fade">
|
||||
<h2>
|
||||
{announcement.title}
|
||||
<aside className="inline ml-1 text-dimmed text-100">
|
||||
<i className="fas fa-clock" />
|
||||
{dayjs(announcement.createdAt).locale(lang.split('_')[0]).fromNow()}
|
||||
</aside>
|
||||
</h2>
|
||||
<section>
|
||||
<ReactMarkdown>{announcement.body}</ReactMarkdown>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { useDispatch } from 'react-redux';
|
||||
import { alertModes } from '../../../common/types/alert-mode';
|
||||
|
@ -44,7 +44,6 @@ type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
|
|||
export const MisshaiPage: React.VFC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const session = useGetSessionQuery(undefined);
|
||||
const [limit, setLimit] = useState<number | undefined>(10);
|
||||
const data = session.data;
|
||||
const score = useGetScoreQuery(undefined);
|
||||
|
||||
|
@ -196,129 +195,113 @@ export const MisshaiPage: React.VFC = () => {
|
|||
<Skeleton width="100%" height="160px" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="vstack fade">
|
||||
<div className="card misshaiData">
|
||||
<div className="body">
|
||||
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
||||
<table className="table fluid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{t('_missHai.dataScore')}</th>
|
||||
<th>{t('_missHai.dataDelta')}</th>
|
||||
</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.data.rating}
|
||||
</p>
|
||||
<article className="fade">
|
||||
<section className="misshaiData">
|
||||
<h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
|
||||
<table className="table fluid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{t('_missHai.dataScore')}</th>
|
||||
<th>{t('_missHai.dataDelta')}</th>
|
||||
</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.data.rating}</p>
|
||||
</section>
|
||||
<section className="misshaiRanking">
|
||||
<h2><i className="fas fa-ranking-star"/> {t('_missHai.ranking')}</h2>
|
||||
<Ranking limit={10} />
|
||||
<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">
|
||||
<input type="checkbox" checked={draft.useRanking} onChange={(e) => {
|
||||
updateSetting({ useRanking: e.target.checked });
|
||||
}}/>
|
||||
<span>{t('_missHai.useRanking')}</span>
|
||||
</label>
|
||||
<h3>{t('alertMode')}</h3>
|
||||
<div className="vstack slim">
|
||||
{ alertModes.map((mode) => (
|
||||
<label key={mode} className="input-check">
|
||||
<input type="radio" checked={mode === draft.alertMode} onChange={() => {
|
||||
updateSetting({ alertMode: mode });
|
||||
}} />
|
||||
<span>{t(`_alertMode.${mode}`)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card misshaiRanking">
|
||||
<div className="body">
|
||||
<h1><i className="bi bi-bar-chart"></i> {t('_missHai.ranking')}</h1>
|
||||
<Ranking limit={10} />
|
||||
<Link to="/apps/miss-hai/ranking" className="btn primary" onClick={() => setLimit(undefined)}>{t('_missHai.showAll')}</Link>
|
||||
<label className="input-check mt-2">
|
||||
<input type="checkbox" checked={draft.useRanking} onChange={(e) => {
|
||||
updateSetting({ useRanking: e.target.checked });
|
||||
}}/>
|
||||
<span>{t('_missHai.useRanking')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="misshaiPageLayout">
|
||||
<div className="card alertModeSetting">
|
||||
<div className="body">
|
||||
<h1><i className="fas fa-gear"></i> {t('alertMode')}</h1>
|
||||
{ (draft.alertMode === 'notification' || draft.alertMode === 'both') && (
|
||||
<div className="alert bg-danger mt-2">
|
||||
<i className="icon fas fa-circle-exclamation"></i>
|
||||
{t('_alertMode.notificationWarning')}
|
||||
</div>
|
||||
)}
|
||||
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
||||
<>
|
||||
<h3>{t('visibility')}</h3>
|
||||
<div className="vstack slim">
|
||||
{ alertModes.map((mode) => (
|
||||
<label key={mode} className="input-check">
|
||||
<input type="radio" checked={mode === draft.alertMode} onChange={() => {
|
||||
updateSetting({ alertMode: mode });
|
||||
}} />
|
||||
<span>{t(`_alertMode.${mode}`)}</span>
|
||||
</label>
|
||||
))}
|
||||
{
|
||||
availableVisibilities.map((visibility) => (
|
||||
<label key={visibility} className="input-check">
|
||||
<input type="radio" checked={visibility === draft.visibility} onChange={() => {
|
||||
updateSetting({ visibility });
|
||||
}} />
|
||||
<span>{t(`_visibility.${visibility}`)}</span>
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{ (draft.alertMode === 'notification' || draft.alertMode === 'both') && (
|
||||
<div className="alert bg-danger mt-2">
|
||||
<i className="icon fas fa-circle-exclamation"></i>
|
||||
{t('_alertMode.notificationWarning')}
|
||||
</div>
|
||||
)}
|
||||
{ (draft.alertMode === 'note' || draft.alertMode === 'both') && (
|
||||
<>
|
||||
<h2 className="mt-2 mb-1">{t('visibility')}</h2>
|
||||
<div className="vstack slim">
|
||||
{
|
||||
availableVisibilities.map((visibility) => (
|
||||
<label key={visibility} className="input-check">
|
||||
<input type="radio" checked={visibility === draft.visibility} onChange={() => {
|
||||
updateSetting({ visibility });
|
||||
}} />
|
||||
<span>{t(`_visibility.${visibility}`)}</span>
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<label className="input-check mt-2">
|
||||
<input type="checkbox" checked={draft.localOnly} onChange={(e) => {
|
||||
updateSetting({ localOnly: e.target.checked });
|
||||
}} />
|
||||
<span>{t('localOnly')}</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<label className="input-check mt-2">
|
||||
<input type="checkbox" checked={draft.localOnly} onChange={(e) => {
|
||||
updateSetting({ localOnly: e.target.checked });
|
||||
}} />
|
||||
<span>{t('localOnly')}</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<h3>{t('template')}</h3>
|
||||
<p>{t('_template.description')}</p>
|
||||
<div className="hstack dense mb-2">
|
||||
<button className="btn" onClick={onClickInsertVariables}>
|
||||
{'{ } '}
|
||||
{t('_template.insertVariables')}
|
||||
</button>
|
||||
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
|
||||
<i className="fas fa-circle-question" />
|
||||
</button>
|
||||
</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>
|
||||
<div className="hstack dense mb-2">
|
||||
<button className="btn" onClick={onClickInsertVariables}>
|
||||
{'{ } '}
|
||||
{t('_template.insertVariables')}
|
||||
</button>
|
||||
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
|
||||
<i className="fas fa-circle-question" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea ref={templateTextarea} className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => {
|
||||
dispatchDraft({ template: e.target.value });
|
||||
}} />
|
||||
<small className="text-dimmed">{t('_template.description2')}</small>
|
||||
<div className="hstack mt-2" style={{justifyContent: 'flex-end'}}>
|
||||
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
|
||||
<button className="btn primary" onClick={() => {
|
||||
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
|
||||
}}>{t('save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea ref={templateTextarea} className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => {
|
||||
dispatchDraft({ template: e.target.value });
|
||||
}} />
|
||||
<small className="text-dimmed">{t('_template.description2')}</small>
|
||||
<div className="hstack mt-2">
|
||||
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}>{t('resetToDefault')}</button>
|
||||
<button className="btn primary" onClick={() => {
|
||||
updateSettingWithDialog({ template: draft.template === '' ? null : draft.template });
|
||||
}}>{t('save')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="list-form mt-2">
|
||||
</section>
|
||||
<section className="list-form mt-2">
|
||||
<button className="item" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>
|
||||
<i className="icon fas fa-paper-plane" />
|
||||
<div className="body">
|
||||
|
@ -326,8 +309,8 @@ export const MisshaiPage: React.VFC = () => {
|
|||
<p className="desc">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ export const RankingPage: React.VFC = () => {
|
|||
useTitle('_missHai.ranking');
|
||||
|
||||
return (
|
||||
<article className="xarticle">
|
||||
<article>
|
||||
<h2>{t('_missHai.ranking')}</h2>
|
||||
<section>
|
||||
<p>{t('_missHai.rankingDescription')}</p>
|
||||
|
|
|
@ -7,8 +7,8 @@ import { IUser } from '../../common/types/user';
|
|||
import { setAccounts } from '../store/slices/screen';
|
||||
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
|
||||
import { $get } from '../misc/api';
|
||||
import { AnnouncementList } from '../components/AnnouncementList';
|
||||
import { DeveloperInfo } from '../components/DeveloperInfo';
|
||||
import { useAnnouncements } from '../hooks/useAnnouncements';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const IndexSessionPage: React.VFC = () => {
|
||||
const {t} = useTranslation();
|
||||
|
@ -16,62 +16,80 @@ export const IndexSessionPage: React.VFC = () => {
|
|||
const { data: session } = useGetSessionQuery(undefined);
|
||||
const score = useGetScoreQuery(undefined);
|
||||
|
||||
const announcements = useAnnouncements();
|
||||
|
||||
useEffect(() => {
|
||||
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[])));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className="vstack fade">
|
||||
<div className="card announcement">
|
||||
<div className="body">
|
||||
<AnnouncementList />
|
||||
<article className="fade">
|
||||
<section>
|
||||
<h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
|
||||
<div className="large menu xmenu fade">
|
||||
{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="card misshaiData">
|
||||
<div className="body">
|
||||
<h1><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h1>
|
||||
<table className="table fluid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{t('_missHai.dataScore')}</th>
|
||||
<th>{t('_missHai.dataDelta')}</th>
|
||||
</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>
|
||||
<section className="misshaiData">
|
||||
<h2><i className="fas fa-chart-line"></i> {t('_missHai.data')}</h2>
|
||||
<table className="table fluid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{t('_missHai.dataScore')}</th>
|
||||
<th>{t('_missHai.dataDelta')}</th>
|
||||
</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>
|
||||
</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>
|
||||
<div className="card developerInfo">
|
||||
<div className="body">
|
||||
<DeveloperInfo />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -6,9 +6,8 @@ import { LoginForm } from '../components/LoginForm';
|
|||
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';
|
||||
import { useAnnouncements } from '../hooks/useAnnouncements';
|
||||
|
||||
const Hero = styled.div<IsMobileProp>`
|
||||
display: flex;
|
||||
|
@ -68,21 +67,10 @@ const FormWrapper = styled.div`
|
|||
`;
|
||||
|
||||
export const IndexWelcomePage: React.VFC = () => {
|
||||
const [announcements, setAnnouncements] = useState<IAnnouncement[]>([]);
|
||||
|
||||
const {isMobile} = useSelector(state => state.screen);
|
||||
const {t} = useTranslation();
|
||||
|
||||
const fetchAllAnnouncements = () => {
|
||||
setAnnouncements([]);
|
||||
$get<IAnnouncement[]>('announcements').then(announcements => {
|
||||
setAnnouncements(announcements ?? []);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllAnnouncements();
|
||||
}, []);
|
||||
const announcements = useAnnouncements();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -96,8 +84,8 @@ export const IndexWelcomePage: React.VFC = () => {
|
|||
</FormWrapper>
|
||||
</div>
|
||||
<div className="announcements">
|
||||
<h2>お知らせ</h2>
|
||||
<div className="menu large">
|
||||
<h2><i className="fas fa-bell"></i> {t('announcements')}</h2>
|
||||
<div className="menu xmenu">
|
||||
{announcements.map(a => (
|
||||
<Link className="item fluid" key={a.id} to={`/announcements/${a.id}`}>
|
||||
{a.title}
|
||||
|
|
|
@ -6,11 +6,33 @@ import { useGetSessionQuery } from '../services/session';
|
|||
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 { changeAccentColor, 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';
|
||||
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 = () => {
|
||||
const session = useGetSessionQuery(undefined);
|
||||
|
@ -23,6 +45,7 @@ export const SettingPage: React.VFC = () => {
|
|||
|
||||
const currentTheme = useSelector(state => state.screen.theme);
|
||||
const currentLang = useSelector(state => state.screen.language);
|
||||
const currentAccentColor = useSelector(state => state.screen.accentColor);
|
||||
|
||||
const onClickLogout = useCallback(() => {
|
||||
dispatch(showModal({
|
||||
|
@ -92,10 +115,10 @@ export const SettingPage: React.VFC = () => {
|
|||
return session.isLoading || !data ? (
|
||||
<div className="skeleton" style={{width: '100%', height: '128px'}}></div>
|
||||
) : (
|
||||
<div className="vstack fade">
|
||||
<Card bodyClassName="vstack">
|
||||
<h1><i className="fas fa-palette"></i> {t('appearance')}</h1>
|
||||
<h2>{t('theme')}</h2>
|
||||
<article className="fade">
|
||||
<h2><i className="fas fa-palette"></i> {t('appearance')}</h2>
|
||||
<section>
|
||||
<h3>{t('theme')}</h3>
|
||||
<div className="vstack">
|
||||
{
|
||||
themes.map(theme => (
|
||||
|
@ -106,11 +129,19 @@ export const SettingPage: React.VFC = () => {
|
|||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2>{t('language')}</h2>
|
||||
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => {
|
||||
dispatch(changeLang(e.target.value));
|
||||
}}>
|
||||
</section>
|
||||
<section>
|
||||
<h3>{t('accentColor')}</h3>
|
||||
<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 => (
|
||||
<option value={n} key={n}>{languageName[n]}</option>
|
||||
|
@ -124,23 +155,26 @@ export const SettingPage: React.VFC = () => {
|
|||
<a href="https://crowdin.com/project/misskey-tools" target="_blank" rel="noopener noreferrer">{t('helpTranslation')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="list-form">
|
||||
<button className="item" onClick={onClickLogout}>
|
||||
<i className="icon fas fa-arrow-up-right-from-square" />
|
||||
<div className="body">
|
||||
<h1>{t('logout')}</h1>
|
||||
<p className="desc">{t('logoutDescription')}</p>
|
||||
</div>
|
||||
</button>
|
||||
<button className="item text-danger" onClick={onClickDeleteAccount}>
|
||||
<i className="icon fas fa-trash-can" />
|
||||
<div className="body">
|
||||
<h1>{t('deleteAccount')}</h1>
|
||||
<p className="desc">{t('deleteAccountDescription')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>その他の設定</h2>
|
||||
<div className="list-form">
|
||||
<button className="item" onClick={onClickLogout}>
|
||||
<i className="icon fas fa-arrow-up-right-from-square" />
|
||||
<div className="body">
|
||||
<h1>{t('logout')}</h1>
|
||||
<p className="desc">{t('logoutDescription')}</p>
|
||||
</div>
|
||||
</button>
|
||||
<button className="item text-danger" onClick={onClickDeleteAccount}>
|
||||
<i className="icon fas fa-trash-can" />
|
||||
<div className="body">
|
||||
<h1>{t('deleteAccount')}</h1>
|
||||
<p className="desc">{t('deleteAccountDescription')}</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
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';
|
||||
import { LOCALSTORAGE_KEY_ACCENT_COLOR, LOCALSTORAGE_KEY_ACCOUNTS, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const';
|
||||
import { Theme } from '../../misc/theme';
|
||||
import { Modal } from '../../modal/modal';
|
||||
import { IUser } from '../../../common/types/user';
|
||||
import { DesignSystemColor } from '../../../common/types/design-system-color';
|
||||
|
||||
interface ScreenState {
|
||||
modal: Modal | null;
|
||||
|
@ -13,6 +14,7 @@ interface ScreenState {
|
|||
theme: Theme;
|
||||
title: string | null;
|
||||
language: string;
|
||||
accentColor: DesignSystemColor;
|
||||
accounts: IUser[];
|
||||
accountTokens: string[];
|
||||
isMobile: boolean;
|
||||
|
@ -24,6 +26,7 @@ const initialState: ScreenState = {
|
|||
modalShown: false,
|
||||
theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system',
|
||||
language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP',
|
||||
accentColor: localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] ?? 'green',
|
||||
title: null,
|
||||
accounts: [],
|
||||
accountTokens: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_ACCOUNTS) || '[]') as string[],
|
||||
|
@ -60,6 +63,9 @@ export const screenSlice = createSlice({
|
|||
localStorage[LOCALSTORAGE_KEY_LANG] = action.payload;
|
||||
i18n.changeLanguage(action.payload);
|
||||
}),
|
||||
changeAccentColor: generateSetter('accentColor', (_, action) => {
|
||||
localStorage[LOCALSTORAGE_KEY_ACCENT_COLOR] = action.payload;
|
||||
}),
|
||||
setAccounts: generateSetter('accounts', (state, action) => {
|
||||
state.accountTokens = action.payload.map(a => a.misshaiToken);
|
||||
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;
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
body {
|
||||
--primary: rgb(134, 179, 0);
|
||||
--primary-d: rgb(52, 70, 0);
|
||||
--max-width: 1024px;
|
||||
font-family: "Koruri", sans-serif;
|
||||
}
|
||||
|
@ -19,6 +17,19 @@ hr {
|
|||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.xmenu {
|
||||
.item {
|
||||
background: #ffffff40;
|
||||
+ .item {
|
||||
margin-top: var(--slim-margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark .xmenu .item {
|
||||
background: #00000040;
|
||||
}
|
||||
|
||||
._header {
|
||||
position: sticky;
|
||||
top: var(--container-padding);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue