0
0
Fork 0

おwipですわ

This commit is contained in:
xeltica 2021-09-04 22:19:35 +09:00
parent b9575d2c5b
commit 8adeb4fe5b
14 changed files with 245 additions and 30 deletions

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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
View 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;
}

View file

@ -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>
);

View 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>
</>
);
};

View file

@ -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
View file

@ -0,0 +1 @@
export const apiEndpoint = `//${location.host}/api/v1/`;

View file

@ -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>
</>
);
};

View 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;

View 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);

View file

@ -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);
}