0
0
Fork 0
This commit is contained in:
Xeltica 2021-09-14 14:28:40 +09:00
parent d06f2384dc
commit 230e952c84
19 changed files with 312 additions and 115 deletions

View file

@ -25,6 +25,7 @@
"dependencies": { "dependencies": {
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@reduxjs/toolkit": "^1.6.1", "@reduxjs/toolkit": "^1.6.1",
"@types/insert-text-at-cursor": "^0.3.0",
"@types/koa-bodyparser": "^4.3.0", "@types/koa-bodyparser": "^4.3.0",
"@types/koa-multer": "^1.0.0", "@types/koa-multer": "^1.0.0",
"@types/koa-send": "^4.1.3", "@types/koa-send": "^4.1.3",
@ -45,6 +46,7 @@
"fibers": "^5.0.0", "fibers": "^5.0.0",
"i18next": "^20.6.1", "i18next": "^20.6.1",
"i18next-browser-languagedetector": "^6.1.2", "i18next-browser-languagedetector": "^6.1.2",
"insert-text-at-cursor": "^0.3.0",
"json5-loader": "^4.0.1", "json5-loader": "^4.0.1",
"koa": "^2.13.0", "koa": "^2.13.0",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom'; import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { IndexPage } from './pages'; import { IndexPage } from './pages';
import { RankingPage } from './pages/ranking'; import { RankingPage } from './pages/ranking';
@ -46,6 +47,8 @@ const AppInner : React.VFC = () => {
}; };
}, [osTheme, setOsTheme]); }, [osTheme, setOsTheme]);
const {t} = useTranslation();
return ( return (
<> <>
<div className="container"> <div className="container">
@ -57,7 +60,7 @@ const AppInner : React.VFC = () => {
</Switch> </Switch>
<footer className="text-center pa-5"> <footer className="text-center pa-5">
<p>(C)2020-2021 Xeltica</p> <p>(C)2020-2021 Xeltica</p>
<p><Link to="/term"></Link></p> <p><Link to="/term">{t('termsOfService')}</Link></p>
</footer> </footer>
<ModalComponent /> <ModalComponent />
</div> </div>

View file

@ -73,7 +73,7 @@ export const ModalComponent: React.VFC = () => {
if (!shown || !modal) return null; if (!shown || !modal) return null;
return ( return (
<div className="modal" onClick={() => dispatch(hideModal())}> <div className="modal fade" onClick={() => dispatch(hideModal())}>
<div className="fade up" onClick={(e) => e.stopPropagation()}> <div className="fade up" onClick={(e) => e.stopPropagation()}>
{ ModalInner(modal) } { ModalInner(modal) }
</div> </div>

View file

@ -1,10 +1,12 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
export const DeveloperInfo: React.VFC = () => { export const DeveloperInfo: React.VFC = () => {
const {t} = useTranslation();
return ( return (
<> <>
<h1></h1> <h1>{t('_developerInfo.title')}</h1>
<p></p> <p>{t('_developerInfo.description')}</p>
<ul> <ul>
<li><a href="http://misskey.io/@ebi" target="_blank" rel="noopener noreferrer">@ebi@misskey.io</a></li> <li><a href="http://misskey.io/@ebi" target="_blank" rel="noopener noreferrer">@ebi@misskey.io</a></li>
<li><a href="http://groundpolis.app/@X" target="_blank" rel="noopener noreferrer">@X@groundpolis.app</a></li> <li><a href="http://groundpolis.app/@X" target="_blank" rel="noopener noreferrer">@X@groundpolis.app</a></li>

View file

@ -1,14 +1,16 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
export type HashtagTimelineProps = { export type HashtagTimelineProps = {
hashtag: string; hashtag: string;
}; };
export const HashtagTimeline: React.VFC<HashtagTimelineProps> = ({hashtag}) => { export const HashtagTimeline: React.VFC<HashtagTimelineProps> = ({hashtag}) => {
const {t} = useTranslation();
return ( return (
<> <>
<h1></h1> <h1>{t('_timeline.title')}</h1>
<p>#{hashtag} </p> <p>{t('_timeline.description', { hashtag })}</p>
<p>WIP</p> <p>WIP</p>
</> </>
); );

View file

@ -8,19 +8,17 @@ export type HeaderProps = {
hasTopLink?: boolean; hasTopLink?: boolean;
}; };
const messageNumber = Math.floor(Math.random() * 6) + 1;
export const Header: React.FC<HeaderProps> = ({hasTopLink, children}) => { export const Header: React.FC<HeaderProps> = ({hasTopLink, children}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const message = React.useMemo(
() => welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)] , []);
return ( return (
<header className={'xarticle card shadow-4 mt-5 mb-3'}> <header className={'xarticle card mt-5 mb-3'}>
<div className="body"> <div className="body">
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}> <h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
{hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')} {hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')}
</h1> </h1>
<h2 className="text-dimmed ml-1">{message}</h2> <h2 className="text-dimmed ml-1">{t(`_welcomeMessage.pattern${messageNumber}`)}</h2>
{children} {children}
</div> </div>
</header> </header>

View file

@ -1,18 +1,19 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
export const LoginForm: React.VFC = () => { export const LoginForm: React.VFC = () => {
const [host, setHost] = useState(''); const [host, setHost] = useState('');
const {t} = useTranslation();
return ( return (
<nav> <nav>
<div> <div>
<strong>URL</strong> <strong>{t('instanceUrl')}</strong>
</div> </div>
<div className="hgroup"> <div className="hgroup">
<input <input
className="input-field" className="input-field"
type="text" type="text"
placeholder="例: misskey.io"
value={host} value={host}
onChange={(e) => setHost(e.target.value)} onChange={(e) => setHost(e.target.value)}
required required
@ -23,7 +24,7 @@ export const LoginForm: React.VFC = () => {
disabled={!host} disabled={!host}
onClick={() => location.href = `//${location.host}/login?host=${encodeURIComponent(host)}`} onClick={() => location.href = `//${location.host}/login?host=${encodeURIComponent(host)}`}
> >
{t('login')}
</button> </button>
</div> </div>
</nav> </nav>

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface RankingResponse { interface RankingResponse {
isCalculating: boolean; isCalculating: boolean;
@ -21,6 +22,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
const [response, setResponse] = useState<RankingResponse | null>(null); const [response, setResponse] = useState<RankingResponse | null>(null);
const [isFetching, setIsFetching] = useState(true); const [isFetching, setIsFetching] = useState(true);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const {t} = useTranslation();
// APIコール // APIコール
useEffect(() => { useEffect(() => {
@ -39,27 +41,27 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
return ( return (
isFetching ? ( isFetching ? (
<p className="text-dimmed"></p> <p className="text-dimmed">{t('fetching')}</p>
) : isError ? ( ) : isError ? (
<div className="alert bg-danger"></div> <div className="alert bg-danger">{t('failedToFetch')}</div>
) : response ? ( ) : response ? (
<> <>
<aside>{response?.userCount}</aside> <aside>{t('registeredUsersCount')}: {response?.userCount}</aside>
{response.isCalculating ? ( {response.isCalculating ? (
<p></p> <p>{t('isCalculating')}</p>
) : ( ) : (
<table className="table shadow-2 mt-1 fluid"> <table className="table mt-1 fluid">
<thead> <thead>
<tr> <tr>
<th></th> <th>{t('_missHai.order')}</th>
<th></th> <th>{t('name')}</th>
<th></th> <th>{t('_missHai.rating')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{response.ranking.map((r, i) => ( {response.ranking.map((r, i) => (
<tr key={i}> <tr key={i}>
<td>{i + 1}</td> <td>{i + 1}</td>
<td> <td>
{r.username}@{r.host} {r.username}@{r.host}
</td> </td>

View file

@ -1,4 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { LOCALSTORAGE_KEY_TOKEN } from '../const'; import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { Skeleton } from './Skeleton'; import { Skeleton } from './Skeleton';
@ -6,6 +7,7 @@ import { Skeleton } from './Skeleton';
export const SessionDataPage: React.VFC = () => { export const SessionDataPage: React.VFC = () => {
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
const {t} = useTranslation();
/** /**
* Session APIのエラーハンドリング * Session APIのエラーハンドリング
@ -30,20 +32,10 @@ export const SessionDataPage: React.VFC = () => {
<div className="fade"> <div className="fade">
{session.data && ( {session.data && (
<section> <section>
<p> <p>{t('welcomeBack', {acct: `@${session.data.username}@${session.data.host}`})}</p>
<a
href={`https://${session.data.host}/@${session.data.username}`}
target="_blank"
rel="noreferrer noopener"
>
@{session.data.username}@{session.data.host}
</a>
</p>
<p> <p>
<strong> <strong>
: {t('_missHai.rating')}:
</strong> </strong>
{session.data.rating} {session.data.rating}
</p> </p>
@ -51,28 +43,28 @@ export const SessionDataPage: React.VFC = () => {
)} )}
{score.data && ( {score.data && (
<section> <section>
<h2></h2> <h2>{t('_missHai.data')}</h2>
<table className="table fluid shadow-2" style={{border: 'none'}}> <table className="table fluid">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th></th> <th>{t('_missHai.dataScore')}</th>
<th></th> <th>{t('_missHai.dataDelta')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td></td> <td>{t('notes')}</td>
<td>{score.data.notesCount}</td> <td>{score.data.notesCount}</td>
<td>{score.data.notesDelta}</td> <td>{score.data.notesDelta}</td>
</tr> </tr>
<tr> <tr>
<td></td> <td>{t('following')}</td>
<td>{score.data.followingCount}</td> <td>{score.data.followingCount}</td>
<td>{score.data.followingDelta}</td> <td>{score.data.followingDelta}</td>
</tr> </tr>
<tr> <tr>
<td></td> <td>{t('followers')}</td>
<td>{score.data.followersCount}</td> <td>{score.data.followersCount}</td>
<td>{score.data.followersDelta}</td> <td>{score.data.followersDelta}</td>
</tr> </tr>

View file

@ -5,12 +5,13 @@ import { Visibility } from '../../common/types/visibility';
import { useGetSessionQuery } from '../services/session'; import { useGetSessionQuery } from '../services/session';
import { defaultTemplate } from '../../common/default-template'; import { defaultTemplate } from '../../common/default-template';
import { Card } from './Card'; import { Card } from './Card';
import { Theme } from '../misc/theme'; import { Theme, themes } from '../misc/theme';
import { API_ENDPOINT, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_TOKEN } from '../const'; import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
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 { useTranslation } from 'react-i18next';
type SettingDraftType = Partial<Pick<IUser, type SettingDraftType = Partial<Pick<IUser,
| 'alertMode' | 'alertMode'
@ -27,6 +28,7 @@ export const SettingPage: React.VFC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const data = session.data; const data = session.data;
const {t} = useTranslation();
const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => { const [draft, dispatchDraft] = useReducer<DraftReducer>((state, action) => {
return { ...state, ...action }; return { ...state, ...action };
@ -38,23 +40,8 @@ export const SettingPage: React.VFC = () => {
template: data?.template ?? null, template: data?.template ?? null,
}); });
const themes: Array<{ theme: Theme, name: string }> = [
{
theme: 'light',
name: 'ライトテーマ'
},
{
theme: 'dark',
name: 'ダークテーマ'
},
{
theme: 'system',
name: 'システム設定に準じる'
},
];
const currentTheme = useSelector(state => state.screen.theme); const currentTheme = useSelector(state => state.screen.theme);
const currentLang = useSelector(state => state.screen.lang); const currentLang = useSelector(state => state.screen.language);
const availableVisibilities: Visibility[] = [ const availableVisibilities: Visibility[] = [
'public', 'public',
@ -150,7 +137,7 @@ export const SettingPage: React.VFC = () => {
) : ( ) : (
<div className="vstack fade"> <div className="vstack fade">
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<h1></h1> <h1>{t('alertMode')}</h1>
<div> <div>
{ {
alertModes.map((mode) => ( alertModes.map((mode) => (
@ -158,27 +145,27 @@ export const SettingPage: React.VFC = () => {
<input type="radio" checked={mode === draft.alertMode} onChange={() => { <input type="radio" checked={mode === draft.alertMode} onChange={() => {
updateSetting({ alertMode: mode }); updateSetting({ alertMode: mode });
}} /> }} />
<span>{mode}</span> <span>{t(`_alertMode.${mode}`)}</span>
</label> </label>
)) ))
} }
{draft.alertMode === 'notification' && ( {draft.alertMode === 'notification' && (
<div className="alert bg-danger mt-2"> <div className="alert bg-danger mt-2">
<i className="icon bi bi-exclamation-circle"></i> <i className="icon bi bi-exclamation-circle"></i>
Misskey Misskeyでは動作しません {t('_alertMode.notificationWarning')}
</div> </div>
)} )}
</div> </div>
{ draft.alertMode === 'note' && ( { draft.alertMode === 'note' && (
<div> <div>
<label htmlFor="visibility" className="input-field"></label> <label htmlFor="visibility" className="input-field">{t('visibility')}</label>
{ {
availableVisibilities.map((visibility) => ( availableVisibilities.map((visibility) => (
<label key={visibility} className="input-check"> <label key={visibility} className="input-check">
<input type="radio" checked={visibility === draft.visibility} onChange={() => { <input type="radio" checked={visibility === draft.visibility} onChange={() => {
updateSetting({ visibility }); updateSetting({ visibility });
}} /> }} />
<span>{visibility}</span> <span>{t(`_visibility.${visibility}`)}</span>
</label> </label>
)) ))
} }
@ -186,26 +173,26 @@ export const SettingPage: React.VFC = () => {
<input type="checkbox" checked={draft.localOnly} onChange={(e) => { <input type="checkbox" checked={draft.localOnly} onChange={(e) => {
updateSetting({ localOnly: e.target.checked }); updateSetting({ localOnly: e.target.checked });
}} /> }} />
<span></span> <span>{t('localOnly')}</span>
</label> </label>
</div> </div>
)} )}
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<h1></h1> <h1>{t('appearance')}</h1>
<h2></h2> <h2>{t('theme')}</h2>
<div> <div>
{ {
themes.map(({ theme, name }) => ( themes.map(theme => (
<label key={theme} className="input-check"> <label key={theme} className="input-check">
<input type="radio" value={theme} checked={theme === currentTheme} onChange={(e) => dispatch(changeTheme(e.target.value as Theme))} /> <input type="radio" value={theme} checked={theme === currentTheme} onChange={(e) => dispatch(changeTheme(e.target.value as Theme))} />
<span>{name}</span> <span>{t(`_themes.${theme}`)}</span>
</label> </label>
)) ))
} }
</div> </div>
<h2></h2> <h2>{t('language')}</h2>
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => { <select name="currentLang" className="input-field" value={currentLang} onChange={(e) => {
dispatch(changeLang(e.target.value)); dispatch(changeLang(e.target.value));
}}> }}>
@ -218,16 +205,14 @@ export const SettingPage: React.VFC = () => {
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<h1></h1> <h1>{t('template')}</h1>
<p>稿</p> <p>{t('_template.description')}</p>
<textarea className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => { <textarea className="input-field" value={draft.template ?? defaultTemplate} placeholder={defaultTemplate} style={{height: 228}} onChange={(e) => {
dispatchDraft({ template: e.target.value }); dispatchDraft({ template: e.target.value });
}} /> }} />
<small className="text-dimmed"> <small className="text-dimmed">{t('_template.description2')}</small>
#misshaialert
</small>
<details> <details>
<summary></summary> <summary>{t('help')}</summary>
<ul className="fade"> <ul className="fade">
<li><code>{'{'}notesCount{'}'}</code>稿</li> <li><code>{'{'}notesCount{'}'}</code>稿</li>
</ul> </ul>
@ -236,26 +221,20 @@ export const SettingPage: React.VFC = () => {
<button className="btn danger" onClick={() => dispatchDraft({ template: null })}></button> <button className="btn danger" onClick={() => dispatchDraft({ template: null })}></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 });
}}></button> }}>{t('save')}</button>
</div> </div>
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<button className="btn block" onClick={onClickSendAlert}></button> <button className="btn block" onClick={onClickSendAlert}>{t('sendAlert')}</button>
<p className="text-dimmed"> <p className="text-dimmed">{t('sendAlertDescription')}</p>
</p>
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<button className="btn block" onClick={onClickLogout}></button> <button className="btn block" onClick={onClickLogout}>{t('logout')}</button>
<p className="text-dimmed"> <p className="text-dimmed">{t('logoutDescription')}</p>
</p>
</Card> </Card>
<Card bodyClassName="vstack"> <Card bodyClassName="vstack">
<button className="btn danger block" onClick={onClickDeleteAccount}></button> <button className="btn danger block" onClick={onClickDeleteAccount}>{t('deleteAccount')}</button>
<p className="text-dimmed"> <p className="text-dimmed">{t('deleteAccountDescription')}</p>
Misskeyとの連携設定を含むみす廃アラートのアカウントを削除します
</p>
</Card> </Card>
</div> </div>
); );

View file

@ -1,5 +1,99 @@
{ {
translation: { translation: {
title: "Misskey Alert", title: "Misskey Tools",
} description1: "Misskeyは楽しいものです。気がついたら1日中入り浸っていることも多いでしょう。",
description2: "さあ、今すぐMisskey Toolsをインストールして、あなたの活動を把握しよう。",
notes: "Notes",
following: "Following",
followers: "Followers",
welcomeBack: "Welcome back, {{acct}}.",
alertMode: "How to Send Alerts",
visibility: "Visibility",
appearance: "Appearance",
template: "Template",
sendAlert: "Test your alert",
sendAlertDescription: "Send your alert with current settings to test.",
logout: "Log Out",
logoutDescription: "If you log out, alerts will be sent.",
deleteAccount: "Deactivate account integration",
deleteAccountDescription: "Delete your Misskey Tools account. This will deactivate integration with Misskey.",
instanceUrl: "Instance URL",
login: "Login",
localOnly: "Local Only",
remoteFollowersOnly: "Remote Followers and Local Only",
help: "Help",
save: "Save",
theme: "Theme",
language: "Language",
fetching: "Fetching...",
failedToFetch: "Failed to fetch",
registeredUsersCount: "Users",
isCalculating: "It is being calculated now. Please check back later!",
ok: "OK",
yes: "Yes",
no: "No",
termsOfService: "Terms of Service",
name: "Name",
_welcomeMessage: {
pattern1: "Overnoting Misskey?",
pattern2: "Overusing Misskey?",
pattern3: "How many notes did you write today?",
pattern4: "Do you really think you're lite-misskist?",
pattern5: "Misskey is my breathing!",
pattern6: "I'm Misskey freak!",
},
_sendAlertDialog: {
title: "Are you sure you want to send the alert for testing?",
message: "Send an alert with the current settings. Be sure to check whether you saved the settings before executing.",
success: "Alert sent.",
error: "Failed to send alert.",
},
_nav: {
data: "Data",
ranking: "Ranking",
settings: "Settings",
},
_missHai: {
ranking: "Miss-hai Ranking",
rankingDescription1: "ユーザーの「みす廃レート」を算出し、高い順にランキング表示しています。みす廃レートは、次のような条件で算出されます。",
rankingFormula: "(ノート数) / (アカウント登録からの経過日数)",
rankingDescription2: "廃人を極めるか、ノート数を控えるか、全てあなた次第!",
showAll: "Show All",
data: "Miss-hai Data",
dataBody: "",
dataScore: "Score",
dataDelta: "Difference",
rating: "Rating",
order: "Order",
},
_developerInfo: {
title: "Developer",
description: "If you want supports, send messages to the below accounts.",
},
_timeline: {
title: "Timeline",
description: "Shows latest notes including {{hashtag}} tags.",
},
_alertMode: {
note: "Automatic Note",
notification: "Notify to your account (Default)",
nothing: "Do Nothing",
notificationWarning: "'Notify to your account' option will not be work on legacy versions of Misskey.",
},
_visibility: {
public: "Public",
home: "Home",
followers: "Followers",
users: "Logged-in Users",
},
_themes: {
light: "Light",
dark: "Dark",
system: "Follows System Preferences",
},
_template: {
description: "アラートの自動投稿をカスタマイズできます。",
description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。",
},
},
} }

View file

@ -1,5 +1,99 @@
{ {
translation: { translation: {
title: "みす廃アラート", title: "Misskey Tools",
} description1: "Misskeyは楽しいものです。気がついたら1日中入り浸っていることも多いでしょう。",
description2: "さあ、今すぐMisskey Toolsをインストールして、あなたの活動を把握しよう。",
notes: "ノート",
following: "フォロー",
followers: "フォロワー",
welcomeBack: "おかえりなさい、{{acct}}さん。",
alertMode: "アラート送信方法",
visibility: "公開範囲",
appearance: "表示設定",
template: "テンプレート",
sendAlert: "アラートをテスト送信する",
sendAlertDescription: "現在の設定を用いて、アラート送信をテストします。",
logout: "ログアウトする",
logoutDescription: "ログアウトしても、アラートは送信されます。",
deleteAccount: "アカウント連携を解除する",
deleteAccountDescription: "Misskey Toolsのアカウントを削除します。これにより、Misskeyとの連携設定も解除されます。",
instanceUrl: "インスタンスURL",
login: "ログイン",
localOnly: "ローカルのみ",
remoteFollowersOnly: "リモートフォロワーとローカル",
help: "ヘルプ",
save: "保存",
theme: "テーマ",
language: "言語",
fetching: "取得中……",
failedToFetch: "取得に失敗しました",
registeredUsersCount: "登録者数",
isCalculating: "現在算出中です。後ほどご確認ください!",
ok: "OK",
yes: "はい",
no: "いいえ",
termsOfService: "利用規約",
name: "名前",
_welcomeMessage: {
'pattern1': 'ついついノートしすぎていませんか?',
'pattern2': 'Misskey, しすぎていませんか?',
'pattern3': '今日、何ノート書いた?',
'pattern4': '10000 ノートは初心者、そう思っていませんか?',
'pattern5': '息するように Misskey、そんなあなたへ。',
'pattern6': 'あなたは真の Misskey 廃人ですか?',
},
_sendAlertDialog: {
title: "アラートをテスト送信しますか?",
message: "現在の設定でアラートを送信します。設定が保存済みであるかどうか、実行前に必ずご確認ください。",
success: "送信しました。",
error: "送信に失敗しました。",
},
_nav: {
data: "データ",
ranking: "ランキング",
settings: "設定",
},
_missHai: {
ranking: "ミス廃ランキング",
rankingDescription1: "ユーザーの「みす廃レート」を算出し、高い順にランキング表示しています。みす廃レートは、次のような条件で算出されます。",
rankingFormula: "(ノート数) / (アカウント登録からの経過日数)",
rankingDescription2: "廃人を極めるか、ノート数を控えるか、全てあなた次第!",
showAll: "全員分見る",
data: "みす廃データ",
dataBody: "内容",
dataScore: "スコア",
dataDelta: "前日比",
rating: "レート",
order: "順位",
},
_developerInfo: {
title: "開発者",
description: "何か困ったことがあったら、以下のアカウントにメッセージを送ってください。",
},
_timeline: {
title: "タイムライン",
description: "{{hashtag}} タグを含む最新ノートを表示します。",
},
_alertMode: {
note: "自動的にノートを投稿",
notification: "Misskeyに通知(標準)",
nothing: "通知しない",
notificationWarning: "「Misskey に通知」オプションは古いMisskeyでは動作しません。",
},
_visibility: {
public: "パブリック",
home: "ホーム",
followers: "フォロワー",
users: "ログインユーザー",
},
_themes: {
light: "ライトテーマ",
dark: "ダークテーマ",
system: "システム設定に準じる",
},
_template: {
description: "アラートの自動投稿をカスタマイズできます。",
description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。",
},
},
} }

View file

@ -1,3 +1,13 @@
export type Theme = ActualTheme | 'system'; export const actualThemes = [
'light',
'dark',
] as const;
export type ActualTheme = 'light' | 'dark'; export const themes = [
...actualThemes,
'system',
] as const;
export type Theme = typeof themes[number];
export type ActualTheme = typeof actualThemes[number];

View file

@ -5,15 +5,17 @@ import { SessionDataPage } from '../components/SessionDataPage';
import { Ranking } from '../components/Ranking'; import { Ranking } from '../components/Ranking';
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';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
const {t, i18n} = useTranslation();
const items = useMemo<TabItem[]>(() => ([ const items = useMemo<TabItem[]>(() => ([
{ label: 'データ' }, { label: t('_nav.data') },
{ label: 'ランキング' }, { label: t('_nav.ranking') },
{ label: '設定' }, { label: t('_nav.settings') },
]), []); ]), [i18n.language]);
const component = useMemo(() => { const component = useMemo(() => {
switch (selectedTab) { switch (selectedTab) {

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Ranking } from '../components/Ranking'; import { Ranking } from '../components/Ranking';
import { LoginForm } from '../components/LoginForm'; import { LoginForm } from '../components/LoginForm';
@ -8,20 +9,22 @@ import { HashtagTimeline } from '../components/HashtagTimeline';
import { Header } from '../components/Header'; import { Header } from '../components/Header';
export const IndexWelcomePage: React.VFC = () => { export const IndexWelcomePage: React.VFC = () => {
const {t} = useTranslation();
return ( return (
<> <>
<Header> <Header>
<article className="mt-4"> <article className="mt-4">
<p>Misskeyは楽しいものです1</p> <p>{t('description1')}</p>
<p></p> <p>{t('description2')}</p>
</article> </article>
<LoginForm /> <LoginForm />
</Header> </Header>
<article className="xarticle card ghost"> <article className="xarticle card ghost">
<div className="body"> <div className="body">
<h1 className="mb-1"></h1> <h1 className="mb-1">{t('_missHai.ranking')}</h1>
<Ranking limit={10} /> <Ranking limit={10} />
<Link to="/ranking"></Link> <Link to="/ranking">{t('_missHai.showAll')}</Link>
</div> </div>
</article> </article>
<article className="xarticle mt-4 row"> <article className="xarticle mt-4 row">

View file

@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { Ranking } from '../components/Ranking'; import { Ranking } from '../components/Ranking';
export const RankingPage: React.VFC = () => { export const RankingPage: React.VFC = () => {
const {t} = useTranslation();
return ( return (
<article className="xarticle"> <article className="xarticle">
<h2></h2> <h2>{t('_missHai.ranking')}</h2>
<section> <section>
<p> <p>{t('_missHai.rankingDescription1')}</p>
<p><strong>{t('_missHai.rankingFormula')}</strong></p>
<p>{t('_missHai.rankingDescription2')}</p>
</p>
<p><strong>() / ()</strong></p>
<p></p>
</section> </section>
<section className="pt-2"> <section className="pt-2">
<Ranking /> <Ranking />

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
export const TermPage: React.VFC = () => { export const TermPage: React.VFC = () => {
// TODO: 外部サイトに誘導する
return ( return (
<article className="xarticle"> <article className="xarticle">
<h2></h2> <h2></h2>

View file

@ -71,8 +71,10 @@ small {
z-index: 40000; z-index: 40000;
} }
.dark .card.dialog { .card.dialog {
background: var(--tone-2); .dark & {
background: var(--tone-2);
}
min-width: min(100vw, 320px); min-width: min(100vw, 320px);
max-width: min(100vw, 600px); max-width: min(100vw, 600px);
} }

View file

@ -411,6 +411,11 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67"
integrity sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q== integrity sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==
"@types/insert-text-at-cursor@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#c7fafad758e26cd7f6bd82c46bf3601fec7afc3e"
integrity sha512-58a+1l6hz5DQ25NjODvZEYmJUWA1ALbUxcf/GHjMfh92r41lZkI6buNN0NwxIvJQDkTFNqL73G2Fduu5ctHVhA==
"@types/json-schema@*", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": "@types/json-schema@*", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8":
version "7.0.9" version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@ -2703,6 +2708,11 @@ ini@^1.3.4, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
insert-text-at-cursor@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
internal-slot@^1.0.3: internal-slot@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"