おwipですわ
This commit is contained in:
parent
b9575d2c5b
commit
8adeb4fe5b
14 changed files with 245 additions and 30 deletions
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { CurrentUser, Get, JsonController } from 'routing-controllers';
|
||||
import { getScores } from '../functions/get-scores';
|
||||
import { User } from '../models/entities/user';
|
||||
|
||||
@JsonController('/session')
|
||||
|
@ -11,4 +12,9 @@ export class SessionController {
|
|||
@Get() get(@CurrentUser({ required: true }) user: User) {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Get('/score')
|
||||
async getScore(@CurrentUser({ required: true }) user: User) {
|
||||
return getScores(user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
|
||||
import { AlertMode, alertModes } from '../../../common/types/alert-mode';
|
||||
import { visibilities, Visibility } from '../../../common/types/visibility';
|
||||
import { IUser } from '../../../common/types/user';
|
||||
|
||||
@Entity()
|
||||
@Index(['username', 'host'], { unique: true })
|
||||
export class User {
|
||||
export class User implements IUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
export type Score = {
|
||||
export interface Score {
|
||||
notesCount: number;
|
||||
followingCount: number;
|
||||
followersCount: number;
|
||||
notesDelta: string;
|
||||
followingDelta: string;
|
||||
followersDelta: string;
|
||||
};
|
||||
}
|
||||
|
|
22
src/common/types/user.ts
Normal file
22
src/common/types/user.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { AlertMode } from './alert-mode';
|
||||
import { Visibility } from './visibility';
|
||||
|
||||
export interface IUser {
|
||||
id: number;
|
||||
username: string;
|
||||
host: string;
|
||||
token: string;
|
||||
misshaiToken: string;
|
||||
prevNotesCount: number;
|
||||
prevFollowingCount: number;
|
||||
prevFollowersCount: number;
|
||||
alertMode: AlertMode;
|
||||
visibility: Visibility;
|
||||
localOnly: boolean;
|
||||
remoteFollowersOnly: boolean;
|
||||
template: string | null;
|
||||
prevRating: number;
|
||||
rating: number;
|
||||
bannedFromRanking: boolean;
|
||||
}
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import { BrowserRouter, Link, Route, Switch, useLocation } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { IndexPage } from './pages';
|
||||
import { RankingPage } from './pages/ranking';
|
||||
import { Header } from './components/Header';
|
||||
import { TermPage } from './pages/term';
|
||||
import { store } from './store';
|
||||
|
||||
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
|
||||
import './style.scss';
|
||||
|
||||
const AppInner : React.VFC = () => {
|
||||
const $location = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container">
|
||||
|
@ -30,7 +33,9 @@ const AppInner : React.VFC = () => {
|
|||
};
|
||||
|
||||
export const App: React.VFC = () => (
|
||||
<BrowserRouter>
|
||||
<AppInner />
|
||||
</BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<AppInner />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
|
44
src/frontend/components/SessionData.tsx
Normal file
44
src/frontend/components/SessionData.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { useGetScoreQuery } from '../services/session';
|
||||
|
||||
export const SessionData: React.VFC = () => {
|
||||
const { data: score, error, isLoading } = useGetScoreQuery(undefined);
|
||||
|
||||
return isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : score === undefined ? (
|
||||
<div>No score</div>
|
||||
) : (
|
||||
<>
|
||||
<section>
|
||||
<h2>みす廃データ</h2>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>内容</th>
|
||||
<th>スコア</th>
|
||||
<th>前日比</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ノート</td>
|
||||
<td>{score.notesCount}</td>
|
||||
<td>{score.notesDelta}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>フォロー</td>
|
||||
<td>{score.followingCount}</td>
|
||||
<td>{score.followingDelta}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>フォロワー</td>
|
||||
<td>{score.followersCount}</td>
|
||||
<td>{score.followersDelta}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export type TabItem = {
|
||||
label: string;
|
||||
|
@ -11,14 +11,14 @@ export type TabProps = {
|
|||
};
|
||||
|
||||
// タブコンポーネント
|
||||
export const Tab: React.FC<TabProps> = (props) => {
|
||||
export const Tab: React.VFC<TabProps> = (props) => {
|
||||
return (
|
||||
<div className="tab">
|
||||
{props.items.map((item, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={'item ' + (index === props.selected ? 'selected' : '')}
|
||||
className={'item ' + (index === props.selected ? 'active' : '')}
|
||||
onClick={() => props.onSelect(index)}
|
||||
>
|
||||
{item.label}
|
||||
|
|
1
src/frontend/const.tsx
Normal file
1
src/frontend/const.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export const apiEndpoint = `//${location.host}/api/v1/`;
|
|
@ -1,27 +1,42 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Header } from '../components/Header';
|
||||
import { SessionData } from '../components/SessionData';
|
||||
import { Tab, TabItem } from '../components/Tab';
|
||||
import { useGetSessionQuery } from '../services/session';
|
||||
|
||||
export const IndexSessionPage: React.VFC = () => {
|
||||
const token = localStorage['token'];
|
||||
const [session, setSession] = useState<Record<string, any> | null>(null);
|
||||
const { data: session, error, isLoading } = useGetSessionQuery(undefined);
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const items = useMemo<TabItem[]>(() => ([
|
||||
{
|
||||
label: 'データ',
|
||||
},
|
||||
{
|
||||
label: 'ランキング',
|
||||
},
|
||||
{
|
||||
label: '設定',
|
||||
},
|
||||
]), []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`//${location.host}/api/v1/session`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
}).then(s => s.json())
|
||||
.then(setSession);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
return isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : error ? (
|
||||
<div>Error: {error}</div>
|
||||
) : (
|
||||
<>
|
||||
<Header>
|
||||
<article className="mt-4">
|
||||
おかえりなさい、{session?.username}さん。
|
||||
</article>
|
||||
</Header>
|
||||
<div className="xarticle card" style={{borderRadius: 'var(--radius)'}}>
|
||||
<Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/>
|
||||
<article className="container">
|
||||
{selectedTab === 0 && <SessionData /> }
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
32
src/frontend/services/session.ts
Normal file
32
src/frontend/services/session.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||
import { apiEndpoint } from '../const';
|
||||
import { IUser } from '../../common/types/user';
|
||||
import { Score } from '../../common/types/score';
|
||||
|
||||
export const sessionApi = createApi({
|
||||
reducerPath: 'session',
|
||||
baseQuery: fetchBaseQuery({ baseUrl: apiEndpoint + 'session' }),
|
||||
endpoints: (builder) => ({
|
||||
getSession: builder.query<IUser, undefined>({
|
||||
query: () => ({
|
||||
url: '/',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage['token']}`,
|
||||
}
|
||||
})
|
||||
}),
|
||||
getScore: builder.query<Score, undefined>({
|
||||
query: () => ({
|
||||
url: '/score',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage['token']}`,
|
||||
}
|
||||
})
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetSessionQuery,
|
||||
useGetScoreQuery,
|
||||
} = sessionApi;
|
21
src/frontend/store/index.ts
Normal file
21
src/frontend/store/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { setupListeners } from '@reduxjs/toolkit/dist/query';
|
||||
import { useSelector as useSelectorBase, TypedUseSelectorHook } from 'react-redux';
|
||||
import { sessionApi } from '../services/session';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
[sessionApi.reducerPath]: sessionApi.reducer,
|
||||
},
|
||||
|
||||
middleware: (defaultMiddleware) => defaultMiddleware()
|
||||
.concat(sessionApi.middleware),
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
|
||||
export const useSelector: TypedUseSelectorHook<RootState> = useSelectorBase;
|
||||
|
||||
setupListeners(store.dispatch);
|
|
@ -6,26 +6,38 @@ body {
|
|||
.xarticle {
|
||||
margin: auto;
|
||||
max-width: 720px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
background: var(--panel);
|
||||
.item {
|
||||
position: relative;
|
||||
padding: var(--margin);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--fg);
|
||||
padding: calc(var(--margin) / 2) var(--margin);
|
||||
&.active {
|
||||
color: var(--primary);
|
||||
transition: width 0.2s ease;
|
||||
&::after {
|
||||
content: "";
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: var(--hover);
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transition: width 0.2s ease;
|
||||
transform-origin: center center;
|
||||
display: block;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue