0
0
Fork 0
This commit is contained in:
xeltica 2021-09-05 10:50:35 +09:00
parent 2c2385ac59
commit d94f2c91fb
11 changed files with 160 additions and 30 deletions

View file

@ -1,18 +1,7 @@
import { config } from '../../config'; import { config } from '../../config';
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { Score } from '../../common/types/score'; import { Score } from '../../common/types/score';
import { defaultTemplate } from '../../common/default-template';
/**
* 稿
*/
export const defaultTemplate = `昨日のMisskeyの活動は
: {notesCount}({notesDelta})
: {followingCount}({followingDelta})
:{followersCount}({followersDelta})
{url}`;
/** /**
* *

View file

@ -9,6 +9,7 @@ import { toSignedString } from './to-signed-string';
* @returns * @returns
*/ */
export const getScores = async (user: User): Promise<Score> => { export const getScores = async (user: User): Promise<Score> => {
// TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも
const miUser = await api<Record<string, number>>(user.host, 'users/show', { username: user.username }, user.token); const miUser = await api<Record<string, number>>(user.host, 'users/show', { username: user.username }, user.token);
if (miUser.error) { if (miUser.error) {
throw miUser.error; throw miUser.error;

View file

@ -13,7 +13,7 @@ import { AlertMode, alertModes } from '../common/types/alert-mode';
import { Users } from './models'; import { Users } from './models';
import { sendAlert } from './services/send-alert'; import { sendAlert } from './services/send-alert';
import { visibilities, Visibility } from '../common/types/visibility'; import { visibilities, Visibility } from '../common/types/visibility';
import { defaultTemplate } from './functions/format'; import { defaultTemplate } from '../common/default-template';
import { die } from './die'; import { die } from './die';
export const router = new Router<DefaultState, Context>(); export const router = new Router<DefaultState, Context>();

View file

@ -14,6 +14,7 @@ html
meta(name='twitter:card' content='summary') meta(name='twitter:card' content='summary')
meta(name='twitter:site' content='@Xeltica') meta(name='twitter:site' content='@Xeltica')
meta(name='twitter:creator' content='@Xeltica') meta(name='twitter:creator' content='@Xeltica')
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css")
style. style.
.loading { .loading {
display: flex; display: flex;
@ -25,7 +26,7 @@ html
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
body.dark body
#app: .loading Loading... #app: .loading Loading...
script(src=`/assets/fe.${version}.js` async defer) script(src=`/assets/fe.${version}.js` async defer)

View file

@ -0,0 +1,11 @@
/**
* 稿
*/
export const defaultTemplate = `昨日のMisskeyの活動は
: {notesCount}({notesDelta})
: {followingCount}({followingDelta})
:{followersCount}({followersDelta})
{url}`;

View file

@ -1,12 +1,18 @@
import React from 'react'; import React from 'react';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { Skeleton } from './Skeleton';
export const SessionData: React.VFC = () => { export const SessionDataPage: React.VFC = () => {
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
return session.isLoading || score.isLoading ? ( return session.isLoading || score.isLoading ? (
<div>Loading...</div> <div className="vstack">
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="1rem" />
<Skeleton width="100%" height="2rem" />
<Skeleton width="100%" height="160px" />
</div>
) : ( ) : (
<> <>
{session.data && ( {session.data && (

View file

@ -0,0 +1,93 @@
import React, { useEffect, useReducer } from 'react';
import { AlertMode } from '../../common/types/alert-mode';
import { IUser } from '../../common/types/user';
import { Visibility } from '../../common/types/visibility';
import { useGetSessionQuery } from '../services/session';
import { defaultTemplate } from '../../common/default-template';
type SettingDraftType = Pick<IUser,
| 'alertMode'
| 'visibility'
| 'localOnly'
| 'remoteFollowersOnly'
| 'template'
>;
type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
export const SettingPage: React.VFC = () => {
const session = useGetSessionQuery(undefined);
const data = session.data;
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,
});
useEffect(() => {
if (data) {
dispatchDraft({
alertMode: data.alertMode,
visibility: data.visibility,
localOnly: data.localOnly,
remoteFollowersOnly: data.remoteFollowersOnly,
template: data.template,
});
}
}, [session.data]);
return session.isLoading || !data ? (
<div className="skeleton" style={{width: '100%', height: '128px'}}></div>
) : (
<div className="vstack">
<div>
<label htmlFor="alertMode" className="input-field"></label>
<select name="alertMode" className="input-field" value={draft.alertMode} onChange={(e) => {
dispatchDraft({ alertMode: e.target.value as AlertMode });
}}>
<option value="note">稿</option>
<option value="notification">Misskeyに通知()</option>
<option value="nothing"></option>
</select>
{draft.alertMode === 'notification' && (
<div className="alert bg-danger mt-2">
<i className="icon bi bi-exclamation-circle"></i>
Misskey Misskeyでは動作しません
</div>
)}
</div>
{ draft.alertMode === 'note' && (
<div>
<label htmlFor="visibility" className="input-field"></label>
<select name="visibility" className="input-field" value={draft.visibility} onChange={(e) => {
dispatchDraft({ visibility: e.target.value as Visibility });
}}>
<option value="public"></option>
<option value="home"></option>
<option value="followers"></option>
</select>
<label className="input-switch mt-2">
<input type="checkbox" />
<div className="switch"></div>
<span></span>
</label>
</div>
)}
<div>
<label htmlFor="template" className="input-field"></label>
<textarea className="input-field" value={draft.template ?? defaultTemplate} style={{height: 256}} onChange={(e) => {
dispatchDraft({ template: e.target.value });
}} />
</div>
<button className="btn primary">
</button>
</div>
);
};

View file

@ -0,0 +1,12 @@
import React from 'react';
export type SkeletonProps = {
width?: string | number;
height?: string | number;
};
export const Skeleton: React.VFC<SkeletonProps> = (p) => {
return (
<div className="skeleton" style={{width: p.width, height: p.height}}></div>
);
};

View file

@ -8,9 +8,6 @@ const token = document.cookie
.find(row => row.startsWith('token')) .find(row => row.startsWith('token'))
?.split('=')[1]; ?.split('=')[1];
console.log(document.cookie);
console.log(token);
if (token) { if (token) {
localStorage['token'] = token; localStorage['token'] = token;
// 今の所はcookieをトークン以外に使用しないため全消去する // 今の所はcookieをトークン以外に使用しないため全消去する

View file

@ -1,29 +1,25 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Header } from '../components/Header'; import { Header } from '../components/Header';
import { SessionData } from '../components/SessionData'; 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';
export const IndexSessionPage: React.VFC = () => { export const IndexSessionPage: React.VFC = () => {
const [selectedTab, setSelectedTab] = useState<number>(0); const [selectedTab, setSelectedTab] = useState<number>(0);
const items = useMemo<TabItem[]>(() => ([ const items = useMemo<TabItem[]>(() => ([
{ { label: 'データ' },
label: 'データ', { label: 'ランキング' },
}, { label: '設定' },
{
label: 'ランキング',
},
{
label: '設定',
},
]), []); ]), []);
const component = useMemo(() => { const component = useMemo(() => {
switch (selectedTab) { switch (selectedTab) {
case 0: return <SessionData />; case 0: return <SessionDataPage />;
case 1: return <Ranking limit={10}/>; case 1: return <Ranking limit={10}/>;
case 2: return <SettingPage/>;
default: return null; default: return null;
} }
}, [selectedTab]); }, [selectedTab]);

View file

@ -9,6 +9,20 @@ body {
overflow: hidden; overflow: hidden;
} }
.skeleton {
border-radius: var(--radius);
background: var(--tone-1);
position: relative;
overflow: hidden;
&::after {
content: "";
inset: 0;
position: absolute;
background: linear-gradient(90deg, transparent, var(--tone-2) 50%, transparent 100%);
animation: 1.2s ease-out 0s infinite skeleton;
}
}
.tab { .tab {
display: flex; display: flex;
background: var(--panel); background: var(--panel);
@ -18,6 +32,7 @@ body {
border: none; border: none;
color: var(--fg); color: var(--fg);
padding: calc(var(--margin) / 2) var(--margin); padding: calc(var(--margin) / 2) var(--margin);
transition: background 0.2s ease;
&.active { &.active {
color: var(--primary); color: var(--primary);
&::after { &::after {
@ -43,3 +58,12 @@ body {
} }
} }
} }
@keyframes skeleton {
from {
transform: translateX(-100%);
}
to {
transform: translateX(300%);
}
}