0
0
Fork 0

レイアウト変更など

This commit is contained in:
xeltica 2021-10-08 12:35:19 +09:00
parent 0c971c3dc8
commit 73adc2130f
5 changed files with 330 additions and 315 deletions

View file

@ -0,0 +1,322 @@
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 { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { showModal } from '../store/slices/screen';
import { Card } from './Card';
import { Ranking } from './Ranking';
import { Skeleton } from './Skeleton';
const variables = [
'notesCount',
'followingCount',
'followersCount',
'notesDelta',
'followingDelta',
'followersDelta',
'url',
'username',
'host',
'rating',
] as const;
type SettingDraftType = Partial<Pick<IUser,
| 'alertMode'
| 'visibility'
| 'localOnly'
| 'remoteFollowersOnly'
| 'template'
>>;
type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
export const MisshaiPage: React.VFC = () => {
const dispatch = useDispatch();
const [limit, setLimit] = useState<number | undefined>(10);
const session = useGetSessionQuery(undefined);
const data = session.data;
const score = useGetScoreQuery(undefined);
const {t} = useTranslation();
const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => {
return { ...state, ...action };
}, {
alertMode: data?.alertMode ?? 'note',
visibility: data?.visibility ?? 'public',
localOnly: data?.localOnly ?? false,
remoteFollowersOnly: data?.remoteFollowersOnly ?? false,
template: data?.template ?? null,
});
const templateTextarea = useRef<HTMLTextAreaElement>(null);
const availableVisibilities: Visibility[] = [
'public',
'home',
'followers'
];
const updateSetting = useCallback((obj: SettingDraftType) => {
const previousDraft = draft;
dispatchDraft(obj);
return fetch(`${API_ENDPOINT}session`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(obj),
})
.catch(e => {
dispatch(showModal({
type: 'dialog',
icon: 'error',
message: 'エラー'
}));
dispatchDraft(previousDraft);
});
}, [draft]);
const updateSettingWithDialog = useCallback((obj: SettingDraftType) => {
updateSetting(obj)
.then(() => dispatch(showModal({
type: 'dialog',
icon: 'info',
message: '保存しました。'
})));
}, [updateSetting]);
useEffect(() => {
if (data) {
dispatchDraft({
alertMode: data.alertMode,
visibility: data.visibility,
localOnly: data.localOnly,
remoteFollowersOnly: data.remoteFollowersOnly,
template: data.template,
});
}
}, [data]);
const onClickInsertVariables = useCallback<React.MouseEventHandler>((e) => {
dispatch(showModal({
type: 'menu',
screenX: e.clientX,
screenY: e.clientY,
items: variables.map(key => ({
name: t('_template._variables.' + key),
onClick: () => {
if (templateTextarea.current) {
insertTextAtCursor(templateTextarea.current, `{${key}}`);
}
},
})),
}));
}, [dispatch, t, variables, templateTextarea.current]);
const onClickInsertVariablesHelp = useCallback(() => {
dispatch(showModal({
type: 'dialog',
icon: 'info',
message: t('_template.insertVariablesHelp'),
}));
}, [dispatch, t]);
const onClickSendAlert = useCallback(() => {
dispatch(showModal({
type: 'dialog',
title: t('_sendTest.title'),
message: t('_sendTest.message'),
icon: 'question',
buttons: [
{
text: t('_sendTest.yes'),
style: 'primary',
},
{
text: t('_sendTest.no'),
},
],
onSelect(i) {
if (i === 0) {
fetch(`${API_ENDPOINT}session/alert`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
},
}).then(() => {
dispatch(showModal({
type: 'dialog',
message: t('_sendTest.success'),
icon: 'info',
}));
}).catch((e) => {
console.error(e);
dispatch(showModal({
type: 'dialog',
message: t('_sendTest.failure'),
icon: 'error',
}));
});
}
},
}));
}, [dispatch, t]);
/**
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
useEffect(() => {
if (session.error) {
console.error(session.error);
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
location.reload();
}
}, [session.error]);
const defaultTemplate = t('_template.default');
return session.isLoading || score.isLoading ? (
<div className="vstack">
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="2rem" />
<Skeleton width="100%" height="160px" />
</div>
) : (
<div className="fade vstack">
{session.data && (
<section>
<p>{t('welcomeBack', {acct: `@${session.data.username}@${session.data.host}`})}</p>
<p>
<strong>
{t('_missHai.rating')}{': '}
</strong>
{session.data.rating}
</p>
</section>
)}
{score.data && (
<>
<section>
<h2>{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>
</section>
<section>
<h2>{t('_missHai.ranking')}</h2>
<Ranking limit={limit} />
{limit && <button className="btn link" onClick={() => setLimit(undefined)}>{t('_missHai.showAll')}</button>}
</section>
<section className="vstack mt-2">
<Card bodyClassName="vstack">
<h1>{t('alertMode')}</h1>
<div>
{
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>
{ draft.alertMode === 'notification' && (
<div className="alert bg-danger mt-2">
<i className="icon bi bi-exclamation-circle"></i>
{t('_alertMode.notificationWarning')}
</div>
)}
{ draft.alertMode === 'note' && (
<>
<h2>{t('visibility')}</h2>
<div>
{
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>
</>
)}
</Card>
<Card bodyClassName="vstack">
<h1>{t('template')}</h1>
<p>{t('_template.description')}</p>
<div className="hstack dense">
<button className="btn" onClick={onClickInsertVariables}>
<i className="bi bi-braces" />&nbsp;
{t('_template.insertVariables')}
</button>
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
<i className="bi bi-question-circle" />
</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" 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>
</Card>
<Card bodyClassName="vstack">
<button className="btn block" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>{t('sendAlert')}</button>
<p className="text-dimmed">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
</Card>
</section>
</>
)}
</div>
);
};

View file

@ -1,77 +0,0 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { Skeleton } from './Skeleton';
export const SessionDataPage: React.VFC = () => {
const session = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined);
const {t} = useTranslation();
/**
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
useEffect(() => {
if (session.error) {
console.error(session.error);
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
location.reload();
}
}, [session.error]);
return session.isLoading || score.isLoading ? (
<div className="vstack">
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="2rem" />
<Skeleton width="100%" height="160px" />
</div>
) : (
<div className="fade">
{session.data && (
<section>
<p>{t('welcomeBack', {acct: `@${session.data.username}@${session.data.host}`})}</p>
<p>
<strong>
{t('_missHai.rating')}{': '}
</strong>
{session.data.rating}
</p>
</section>
)}
{score.data && (
<section>
<h2>{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>
</section>
)}
</div>
);
};

View file

@ -1,10 +1,7 @@
import React, { useCallback, useEffect, useReducer, useRef } from 'react'; import React, { useCallback } 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 { IUser } from '../../common/types/user';
import { Visibility } from '../../common/types/visibility';
import { useGetSessionQuery } from '../services/session'; import { useGetSessionQuery } from '../services/session';
import { Card } from './Card'; import { Card } from './Card';
import { Theme, themes } from '../misc/theme'; import { Theme, themes } from '../misc/theme';
@ -12,30 +9,6 @@ import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { changeLang, changeTheme, showModal } from '../store/slices/screen'; import { changeLang, changeTheme, showModal } from '../store/slices/screen';
import { useSelector } from '../store'; import { useSelector } from '../store';
import { languageName } from '../langs'; import { languageName } from '../langs';
import insertTextAtCursor from 'insert-text-at-cursor';
const variables = [
'notesCount',
'followingCount',
'followersCount',
'notesDelta',
'followingDelta',
'followersDelta',
'url',
'username',
'host',
'rating',
] as const;
type SettingDraftType = Partial<Pick<IUser,
| 'alertMode'
| 'visibility'
| 'localOnly'
| 'remoteFollowersOnly'
| 'template'
>>;
type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
export const SettingPage: React.VFC = () => { export const SettingPage: React.VFC = () => {
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
@ -44,134 +17,9 @@ export const SettingPage: React.VFC = () => {
const data = session.data; const data = session.data;
const {t} = useTranslation(); const {t} = useTranslation();
const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => {
return { ...state, ...action };
}, {
alertMode: data?.alertMode ?? 'note',
visibility: data?.visibility ?? 'public',
localOnly: data?.localOnly ?? false,
remoteFollowersOnly: data?.remoteFollowersOnly ?? false,
template: data?.template ?? null,
});
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 templateTextarea = useRef<HTMLTextAreaElement>(null);
const availableVisibilities: Visibility[] = [
'public',
'home',
'followers'
];
const updateSetting = useCallback((obj: SettingDraftType) => {
const previousDraft = draft;
dispatchDraft(obj);
return fetch(`${API_ENDPOINT}session`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(obj),
})
.catch(e => {
dispatch(showModal({
type: 'dialog',
icon: 'error',
message: 'エラー'
}));
dispatchDraft(previousDraft);
});
}, [draft]);
const updateSettingWithDialog = useCallback((obj: SettingDraftType) => {
updateSetting(obj)
.then(() => dispatch(showModal({
type: 'dialog',
icon: 'info',
message: '保存しました。'
})));
}, [updateSetting]);
useEffect(() => {
if (data) {
dispatchDraft({
alertMode: data.alertMode,
visibility: data.visibility,
localOnly: data.localOnly,
remoteFollowersOnly: data.remoteFollowersOnly,
template: data.template,
});
}
}, [data]);
const onClickInsertVariables = useCallback<React.MouseEventHandler>((e) => {
dispatch(showModal({
type: 'menu',
screenX: e.clientX,
screenY: e.clientY,
items: variables.map(key => ({
name: t('_template._variables.' + key),
onClick: () => {
if (templateTextarea.current) {
insertTextAtCursor(templateTextarea.current, `{${key}}`);
}
},
})),
}));
}, [dispatch, t, variables, templateTextarea.current]);
const onClickInsertVariablesHelp = useCallback(() => {
dispatch(showModal({
type: 'dialog',
icon: 'info',
message: t('_template.insertVariablesHelp'),
}));
}, [dispatch, t]);
const onClickSendAlert = useCallback(() => {
dispatch(showModal({
type: 'dialog',
title: t('_sendTest.title'),
message: t('_sendTest.message'),
icon: 'question',
buttons: [
{
text: t('_sendTest.yes'),
style: 'primary',
},
{
text: t('_sendTest.no'),
},
],
onSelect(i) {
if (i === 0) {
fetch(`${API_ENDPOINT}session/alert`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
},
}).then(() => {
dispatch(showModal({
type: 'dialog',
message: t('_sendTest.success'),
icon: 'info',
}));
}).catch((e) => {
console.error(e);
dispatch(showModal({
type: 'dialog',
message: t('_sendTest.failure'),
icon: 'error',
}));
});
}
},
}));
}, [dispatch, t]);
const onClickLogout = useCallback(() => { const onClickLogout = useCallback(() => {
dispatch(showModal({ dispatch(showModal({
type: 'dialog', type: 'dialog',
@ -241,57 +89,11 @@ export const SettingPage: React.VFC = () => {
})); }));
}, [dispatch, t]); }, [dispatch, t]);
const defaultTemplate = t('_template.default');
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"> <div className="vstack fade">
<Card bodyClassName="vstack">
<h1>{t('alertMode')}</h1>
<div>
{
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>
{ draft.alertMode === 'notification' && (
<div className="alert bg-danger mt-2">
<i className="icon bi bi-exclamation-circle"></i>
{t('_alertMode.notificationWarning')}
</div>
)}
{ draft.alertMode === 'note' && (
<>
<h2>{t('visibility')}</h2>
<div>
{
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>
</>
)}
</Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<h1>{t('appearance')}</h1> <h1>{t('appearance')}</h1>
<h2>{t('theme')}</h2> <h2>{t('theme')}</h2>
@ -322,34 +124,6 @@ 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>
</Card> </Card>
<Card bodyClassName="vstack">
<h1>{t('template')}</h1>
<p>{t('_template.description')}</p>
<div className="hstack dense">
<button className="btn" onClick={onClickInsertVariables}>
<i className="bi bi-braces" />&nbsp;
{t('_template.insertVariables')}
</button>
<button className="btn link text-info" onClick={onClickInsertVariablesHelp}>
<i className="bi bi-question-circle" />
</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" 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>
</Card>
<Card bodyClassName="vstack">
<button className="btn block" onClick={onClickSendAlert} disabled={draft.alertMode === 'nothing'}>{t('sendAlert')}</button>
<p className="text-dimmed">{t(draft.alertMode === 'nothing' ? 'sendAlertDisabled' : 'sendAlertDescription')}</p>
</Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<button className="btn block" onClick={onClickLogout}>{t('logout')}</button> <button className="btn block" onClick={onClickLogout}>{t('logout')}</button>
<p className="text-dimmed">{t('logoutDescription')}</p> <p className="text-dimmed">{t('logoutDescription')}</p>

View file

@ -58,15 +58,14 @@
"nextFeaturesDescription": "これだけではありません。今後もアップデートで様々な機能を追加します!" "nextFeaturesDescription": "これだけではありません。今後もアップデートで様々な機能を追加します!"
}, },
"_nav": { "_nav": {
"data": "データ", "misshai": "ミス廃",
"ranking": "ランキング",
"settings": "設定" "settings": "設定"
}, },
"_missHai": { "_missHai": {
"ranking": "ミス廃ランキング", "ranking": "ミス廃ランキング",
"rankingDescription": "ユーザーの「みす廃レート」を算出し、高い順にランキング表示しています。", "rankingDescription": "ユーザーの「ミス廃レート」を算出し、高い順にランキング表示しています。",
"showAll": "全員分見る", "showAll": "全員分見る",
"data": "みす廃データ", "data": "ミス廃データ",
"dataBody": "内容", "dataBody": "内容",
"dataScore": "スコア", "dataScore": "スコア",
"dataDelta": "前日比", "dataDelta": "前日比",

View file

@ -1,11 +1,10 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Header } from '../components/Header'; import { Header } from '../components/Header';
import { SessionDataPage } from '../components/SessionDataPage'; import { MisshaiPage } from '../components/MisshaiPage';
import { Tab, TabItem } from '../components/Tab'; import { Tab, TabItem } from '../components/Tab';
import { SettingPage } from '../components/SettingPage'; import { SettingPage } from '../components/SettingPage';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RankingPage } from '../components/RankingPage';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
@ -13,16 +12,14 @@ export const IndexSessionPage: React.VFC = () => {
const {t, i18n} = useTranslation(); const {t, i18n} = useTranslation();
const items = useMemo<TabItem[]>(() => ([ const items = useMemo<TabItem[]>(() => ([
{ label: t('_nav.data') }, { label: t('_nav.misshai') },
{ label: t('_nav.ranking') },
{ label: t('_nav.settings') }, { label: t('_nav.settings') },
]), [i18n.language]); ]), [i18n.language]);
const component = useMemo(() => { const component = useMemo(() => {
switch (selectedTab) { switch (selectedTab) {
case 0: return <SessionDataPage />; case 0: return <MisshaiPage />;
case 1: return <RankingPage/>; case 1: return <SettingPage/>;
case 2: return <SettingPage/>;
default: return null; default: return null;
} }
}, [selectedTab]); }, [selectedTab]);