レイアウト変更など
This commit is contained in:
parent
0c971c3dc8
commit
73adc2130f
5 changed files with 330 additions and 315 deletions
322
src/frontend/components/MisshaiPage.tsx
Normal file
322
src/frontend/components/MisshaiPage.tsx
Normal 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" />
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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" />
|
|
||||||
{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>
|
||||||
|
|
|
@ -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": "前日比",
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue