0
0
Fork 0
This commit is contained in:
xeltica 2021-09-01 20:06:33 +09:00
parent 66ced29c6d
commit dbcfdcd0c3
41 changed files with 3637 additions and 791 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*.{ts,tsx,js,json,pug}]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_size = 2

View file

@ -18,7 +18,7 @@ module.exports = {
'rules': { 'rules': {
'indent': [ 'indent': [
'error', 'error',
'tab' 'tab',
], ],
'linebreak-style': [ 'linebreak-style': [
'error', 'error',

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View file

@ -1,5 +1,5 @@
{ {
"watch": ["src"], "watch": ["src"],
"ext": "ts", "ext": "ts,tsx,pug,scss",
"exec": "run-s build start" "exec": "run-s build start"
} }

View file

@ -8,45 +8,70 @@
"scripts": { "scripts": {
"tsc": "tsc", "tsc": "tsc",
"start": "node built/app.js", "start": "node built/app.js",
"lint": "eslint src/index.ts", "lint": "eslint --ext .ts,.tsx src",
"lint:fix": "eslint --fix src/index.ts", "lint:fix": "eslint --fix --ext .ts,.tsx src",
"clean": "rimraf built", "clean": "rimraf built",
"build:backend": "tsc", "build:backend": "tsc",
"build:frontend": "webpack",
"build:views": "copyfiles -u 1 src/views/*.pug ./built/",
"build:styles": "sass styles/:built/assets",
"build": "run-p build:*", "build": "run-p build:*",
"migrate": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:run", "migrate": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:run",
"migrate:revert": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:revert", "migrate:revert": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:revert",
"dev": "nodemon" "dev": "nodemon",
"dev:frontend": "webpack --watch"
}, },
"dependencies": { "dependencies": {
"@babel/preset-react": "^7.14.5",
"@types/koa-bodyparser": "^4.3.0", "@types/koa-bodyparser": "^4.3.0",
"@types/koa-mount": "^4.0.0",
"@types/koa-multer": "^1.0.0", "@types/koa-multer": "^1.0.0",
"@types/koa-static": "^4.0.1", "@types/koa-send": "^4.1.3",
"@types/ms": "^0.7.31",
"@types/node-cron": "^2.0.3", "@types/node-cron": "^2.0.3",
"@types/object.pick": "^1.3.1",
"@types/react": "^17.0.19",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.13",
"@types/uuid": "^8.0.0", "@types/uuid": "^8.0.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"class-transformer": "^0.4.0", "class-transformer": "^0.4.0",
"class-validator": "^0.13.1", "class-validator": "^0.13.1",
"css-loader": "^6.2.0",
"dayjs": "^1.10.2", "dayjs": "^1.10.2",
"delay": "^4.4.0", "delay": "^4.4.0",
"fibers": "^5.0.0",
"json5-loader": "^4.0.1",
"koa": "^2.13.0", "koa": "^2.13.0",
"koa-bodyparser": "^4.3.0", "koa-bodyparser": "^4.3.0",
"koa-mount": "^4.0.0",
"koa-multer": "^1.0.2", "koa-multer": "^1.0.2",
"koa-router": "^9.1.0", "koa-router": "^9.1.0",
"koa-send": "^5.0.1",
"koa-session": "^6.0.0", "koa-session": "^6.0.0",
"koa-static": "^5.0.0",
"koa-views": "^6.3.0", "koa-views": "^6.3.0",
"misskey-js": "^0.0.6",
"ms": "^2.1.3",
"node-cron": "^2.0.3", "node-cron": "^2.0.3",
"object.pick": "^1.3.0",
"pg": "^8.3.0", "pg": "^8.3.0",
"pug": "^3.0.0", "pug": "^3.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rndstr": "^1.0.0", "rndstr": "^1.0.0",
"routing-controllers": "^0.9.0", "routing-controllers": "^0.9.0",
"sass": "^1.26.10", "sass": "^1.38.2",
"sass-loader": "^12.1.0",
"style-loader": "^3.2.1",
"styled-components": "^5.3.1",
"ts-loader": "^9.2.5",
"typeorm": "0.2.25", "typeorm": "0.2.25",
"typescript": "^3.9.7", "typescript": "^4.4.2",
"uuid": "^8.3.0" "uuid": "^8.3.0",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"xeltica-ui": "xeltica/ui"
}, },
"devDependencies": { "devDependencies": {
"@types/axios": "^0.14.0", "@types/axios": "^0.14.0",
@ -55,10 +80,10 @@
"@types/koa-session": "^5.10.2", "@types/koa-session": "^5.10.2",
"@types/koa-views": "^2.0.4", "@types/koa-views": "^2.0.4",
"@types/node": "^8.0.29", "@types/node": "^8.0.29",
"@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^3.7.0", "@typescript-eslint/parser": "^4.30.0",
"copyfiles": "^2.3.0", "copyfiles": "^2.3.0",
"eslint": "^7.5.0", "eslint": "^7.32.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4", "eslint-plugin-prettier": "^3.1.4",
"nodemon": "^2.0.4", "nodemon": "^2.0.4",

View file

@ -1,7 +0,0 @@
import { MetaController } from "./meta";
import { RankingController } from "./ranking";
export default [
MetaController,
RankingController,
];

View file

@ -1,9 +1,13 @@
import { Get, JsonController } from "routing-controllers"; /**
* API
* @author Xeltica
*/
@JsonController() import { Get, JsonController } from 'routing-controllers';
@JsonController('/meta')
export class MetaController { export class MetaController {
@Get('/meta') @Get() get() {
get() {
return { return {
honi: 'ほに', honi: 'ほに',
}; };

View file

@ -1,21 +1,31 @@
import { Get, JsonController, QueryParam } from "routing-controllers"; /**
import { getRanking } from "../functions/ranking"; * API
import { getState } from "../store"; * @author Xeltica
*/
@JsonController() import { Get, JsonController, QueryParam } from 'routing-controllers';
import { getRanking } from '../functions/ranking';
import { getUserCount } from '../functions/users';
import { getState } from '../store';
@JsonController('/ranking')
export class RankingController { export class RankingController {
@Get('/ranking') @Get()
get( async get(@QueryParam('limit', { required: false }) limit?: string) {
@QueryParam('limit', { type: Number, required: false }) return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
limit?: number
) {
return this.getResponse(getState().nowCalculating)
} }
private getResponse(isCalculating: boolean, limit?: number) { private async getResponse(isCalculating: boolean, limit?: number) {
const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({
id: u.id,
username: u.username,
host: u.host,
rating: u.rating,
}));
return { return {
isCalculating, isCalculating,
ranking: isCalculating ? [] : getRanking(limit), userCount: await getUserCount(),
ranking,
}; };
} }
} }

View file

@ -0,0 +1,14 @@
/**
* API
* @author Xeltica
*/
import { CurrentUser, Get, JsonController } from 'routing-controllers';
import { User } from '../models/entities/user';
@JsonController('/session')
export class SessionController {
@Get() get(@CurrentUser({ required: true }) user: User) {
return user;
}
}

6
src/die.ts Normal file
View file

@ -0,0 +1,6 @@
import { Context } from 'koa';
export const die = (ctx: Context, error = '問題が発生しました。お手数ですが、最初からやり直してください。', status = 400): Promise<void> => {
ctx.status = status;
return ctx.render('error', { error });
};

33
src/frontend/App.tsx Normal file
View file

@ -0,0 +1,33 @@
import * as React from 'react';
import { BrowserRouter, Route, Switch, useLocation } from 'react-router-dom';
import { IndexPage } from './pages';
import { RankingPage } from './pages/ranking';
import { Header } from './components/Header';
import 'xeltica-ui/dist/css/xeltica-ui.min.css';
import './style.scss';
const AppInner : React.VFC = () => {
const $location = useLocation();
return (
<>
<div className="container">
{$location.pathname !== '/' && <Header hasTopLink />}
<Switch>
<Route exact path="/" component={IndexPage} />
<Route exact path="/ranking" component={RankingPage} />
</Switch>
<footer className="text-center pa-5">
(C)2020-2021 Xeltica
</footer>
</div>
</>
);
};
export const App: React.VFC = () => (
<BrowserRouter>
<AppInner />
</BrowserRouter>
);

View file

@ -0,0 +1,15 @@
import React from 'react';
export const DeveloperInfo: React.VFC = () => {
return (
<>
<h1></h1>
<p></p>
<ul>
<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://twitter.com/@adxlw" target="_blank" rel="noopener noreferrer">@adxlw@twitter.com</a></li>
</ul>
</>
);
};

View file

@ -0,0 +1,15 @@
import React from 'react';
export type HashtagTimelineProps = {
hashtag: string;
};
export const HashtagTimeline: React.VFC<HashtagTimelineProps> = ({hashtag}) => {
return (
<>
<h1></h1>
<p>#{hashtag} </p>
<p>WIP</p>
</>
);
};

View file

@ -0,0 +1,24 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { welcomeMessage } from '../../misc/welcome-message';
export type HeaderProps = {
hasTopLink?: boolean;
};
export const Header: React.FC<HeaderProps> = ({hasTopLink, children}) => {
const message = React.useMemo(
() => welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)] , []);
return (
<header className={'xarticle card shadow-4 mt-5 mb-3'}>
<div className="body">
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
{hasTopLink ? <Link to="/"></Link> : 'みす廃アラート'}
</h1>
<h2 className="text-dimmed ml-1">{message}</h2>
{children}
</div>
</header>
);
};

View file

@ -0,0 +1,31 @@
import React, { useState } from 'react';
export const LoginForm: React.VFC = () => {
const [host, setHost] = useState('');
return (
<nav>
<div>
<strong>URL</strong>
</div>
<div className="hgroup">
<input
className="input-field"
type="text"
placeholder="例: misskey.io"
value={host}
onChange={(e) => setHost(e.target.value)}
required
/>
<button
className={!host ? 'btn' : 'btn primary'}
style={{ width: 128 }}
disabled={!host}
onClick={() => location.href = `//${location.host}/login?host=${encodeURIComponent(host)}`}
>
</button>
</div>
</nav>
);
};

View file

@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
interface RankingResponse {
isCalculating: boolean;
userCount: number;
ranking: Ranking[];
}
interface Ranking {
id: number;
username: string;
host: string;
rating: number;
}
export type RankingProps = {
limit?: number;
};
export const Ranking: React.VFC<RankingProps> = ({limit}) => {
const [response, setResponse] = useState<RankingResponse | null>(null);
const [isFetching, setIsFetching] = useState(true);
const [isError, setIsError] = useState(false);
// APIコール
useEffect(() => {
setIsFetching(true);
fetch(`//${location.host}/api/v1/ranking?limit=${limit ?? ''}`)
.then((r) => (r.json() as unknown) as RankingResponse)
.then((result) => {
setResponse(result);
setIsFetching(false);
})
.catch(c => {
console.error(c);
setIsError(true);
});
}, [limit, setIsFetching, setIsError]);
return (
isFetching ? (
<p className="text-dimmed"></p>
) : isError ? (
<div className="alert bg-danger"></div>
) : response ? (
<>
<aside>{response?.userCount}</aside>
{response.isCalculating ? (
<p></p>
) : (
<table className="table shadow-2 mt-1 fluid">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{response.ranking.map((r, i) => (
<tr key={i}>
<td>{i + 1}</td>
<td>
{r.username}@{r.host}
</td>
<td>{r.rating}</td>
</tr>
))}
</tbody>
</table>
)}
</>
) : null
);
};

5
src/frontend/init.tsx Normal file
View file

@ -0,0 +1,5 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App/>, document.getElementById('app'));

View file

@ -0,0 +1,45 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Ranking } from '../components/Ranking';
import { LoginForm } from '../components/LoginForm';
import { DeveloperInfo } from '../components/DeveloperInfo';
import { HashtagTimeline } from '../components/HashtagTimeline';
import { Header } from '../components/Header';
export const IndexPage: React.VFC = () => {
return (
<>
<Header>
<article className="mt-4">
<p>
Misskeyは楽しいものです1
</p>
<p>
</p>
</article>
<LoginForm />
</Header>
<article className="xarticle card ghost">
<div className="body">
<h1 className="mb-1"></h1>
<Ranking limit={10} />
<Link to="/ranking"></Link>
</div>
</article>
<article className="xarticle mt-4 row">
<div className="col-12 pc-6 card ghost">
<div className="body"><DeveloperInfo/></div>
</div>
<div className="col-12 pc-6 card ghost">
<div className="body"><HashtagTimeline hashtag="misshaialert"/></div>
</div>
</article>
<footer className="text-center pa-5">
(C)2020-2021 Xeltica
</footer>
</>
);
};

View file

@ -0,0 +1,19 @@
import React from 'react';
import { Ranking } from '../components/Ranking';
export const RankingPage: React.VFC = () => {
return (
<article className="xarticle">
<h2></h2>
<section>
<p></p>
<p><strong>() / ()</strong></p>
<p></p>
</section>
<section className="pt-2">
<Ranking />
</section>
</article>
);
};

9
src/frontend/style.scss Normal file
View file

@ -0,0 +1,9 @@
body {
--primary: rgb(134, 179, 0);
--shadow-color: rgba(0, 0, 0, 0.1);
}
.xarticle {
margin: auto;
max-width: 720px;
}

View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"strictNullChecks": true,
"noUnusedLocals" : true,
"noImplicitThis": true,
"alwaysStrict": true,
"outDir": "./built/assets/",
"sourceMap": true,
"noImplicitAny": true,
"lib": ["dom"],
"module": "ESNext",
"target": "es5",
"moduleResolution": "node",
"jsx": "react",
"allowSyntheticDefaultImports": true
}
}

View file

@ -2,6 +2,7 @@ import { User } from '../models/entities/user';
import { Users } from '../models'; import { Users } from '../models';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
import { genToken } from './gen-token'; import { genToken } from './gen-token';
import pick from 'object.pick';
export const getUser = (username: string, host: string): Promise<User | undefined> => { export const getUser = (username: string, host: string): Promise<User | undefined> => {
return Users.findOne({ username, host }); return Users.findOne({ username, host });

10
src/render.ts Normal file
View file

@ -0,0 +1,10 @@
import views from 'koa-views';
import constant from './const';
export const render = views(__dirname + '/views', {
extension: 'pug', options: {
...constant,
}
});

View file

@ -2,87 +2,25 @@ import { Context, DefaultState } from 'koa';
import Router from 'koa-router'; import Router from 'koa-router';
import axios from 'axios'; import axios from 'axios';
import crypto from 'crypto'; import crypto from 'crypto';
import koaSend from 'koa-send';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { config } from '../config'; import ms from 'ms';
import { upsertUser, getUser, getUserCount, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from '../functions/users';
import { api, apiAvailable } from '../services/misskey'; import { config } from './config';
import { getScores } from '../functions/get-scores'; import { upsertUser, getUser, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from './functions/users';
import { AlertMode, alertModes } from '../types/AlertMode'; import { api } from './services/misskey';
import { Users } from '../models'; import { AlertMode, alertModes } from './types/alert-mode';
import { send } from '../services/send'; import { Users } from './models';
import { visibilities, Visibility } from '../types/Visibility'; import { send } from './services/send';
import { defaultTemplate, variables } from '../functions/format'; import { visibilities, Visibility } from './types/visibility';
import { getRanking } from '../functions/ranking'; import { defaultTemplate } from './functions/format';
import { getState } from '../store'; import { die } from './die';
import { welcomeMessage } from '../misc/welcome-message';
export const router = new Router<DefaultState, Context>(); export const router = new Router<DefaultState, Context>();
const sessionHostCache: Record<string, string> = { }; const sessionHostCache: Record<string, string> = { };
const tokenSecretCache: Record<string, string> = { }; const tokenSecretCache: Record<string, string> = { };
const login = async (ctx: Context, user: Record<string, unknown>, host: string, token: string) => {
const isNewcomer = !(await getUser(user.username as string, host));
await upsertUser(user.username as string, host, token);
const u = await getUser(user.username as string, host);
if (!u) {
await die(ctx);
return;
}
if (isNewcomer) {
await updateUser(u.username, u.host, {
prevNotesCount: user.notesCount as number ?? 0,
prevFollowingCount: user.followingCount as number ?? 0,
prevFollowersCount: user.followersCount as number ?? 0,
});
}
const misshaiToken = await updateUsersMisshaiToken(u);
ctx.cookies.set('token', misshaiToken, { signed: true });
// await ctx.render('logined', { user: u });
ctx.redirect('/');
};
router.get('/', async ctx => {
const token = ctx.cookies.get('token', { signed: true });
const user = token ? await getUserByMisshaiToken(token) : undefined;
const isAvailable = user && await apiAvailable(user.host, user.token);
const usersCount = await getUserCount();
const ranking = await getRanking(10);
const commonLocals = {
usersCount, ranking,
state: getState(),
from: ctx.query.from,
};
if (user && isAvailable) {
const meta = await api<{ version: string }>(user?.host, 'meta', {});
await ctx.render('mypage', {
...commonLocals,
user,
// To Activate Groundpolis Mode
isGroundpolis: meta.version.includes('gp'),
defaultTemplate,
templateVariables: variables,
score: await getScores(user),
});
} else {
// 非ログイン
await ctx.render('welcome', {
...commonLocals,
welcomeMessage: welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)],
});
}
});
router.get('/login', async ctx => { router.get('/login', async ctx => {
let host = ctx.query.host as string | undefined; let host = ctx.query.host as string | undefined;
@ -135,13 +73,6 @@ router.get('/teapot', async ctx => {
await die(ctx, 'I\'m a teapot', 418); await die(ctx, 'I\'m a teapot', 418);
}); });
router.get('/ranking', async ctx => {
await ctx.render('ranking', {
state: getState(),
ranking: await getRanking(null),
});
});
router.get('/miauth', async ctx => { router.get('/miauth', async ctx => {
const session = ctx.query.session as string | undefined; const session = ctx.query.session as string | undefined;
if (!session) { if (!session) {
@ -285,8 +216,44 @@ router.post('/send', async ctx => {
ctx.redirect('/?from=send'); ctx.redirect('/?from=send');
}); });
router.get('/assets/(.*)', async ctx => {
// Return 404 for other pages await koaSend(ctx as any, ctx.path.replace('/assets/', ''), {
router.all('(.*)', async ctx => { root: `${__dirname}/assets/`,
await die(ctx, 'ページが見つかりませんでした', 404); maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'),
});
}); });
router.get('/api(.*)', async (ctx, next) => {
next();
});
router.get('(.*)', async (ctx) => {
await ctx.render('frontend');
});
async function login(ctx: Context, user: Record<string, unknown>, host: string, token: string) {
const isNewcomer = !(await getUser(user.username as string, host));
await upsertUser(user.username as string, host, token);
const u = await getUser(user.username as string, host);
if (!u) {
await die(ctx);
return;
}
if (isNewcomer) {
await updateUser(u.username, u.host, {
prevNotesCount: user.notesCount as number ?? 0,
prevFollowingCount: user.followingCount as number ?? 0,
prevFollowersCount: user.followersCount as number ?? 0,
});
}
const misshaiToken = await updateUsersMisshaiToken(u);
ctx.cookies.set('token', misshaiToken, { signed: true });
// await ctx.render('logined', { user: u });
ctx.redirect('/');
}

View file

@ -1,26 +1,39 @@
import Koa from 'koa';
import bodyParser from 'koa-bodyparser'; import bodyParser from 'koa-bodyparser';
import serve from 'koa-static'; import { Action, useKoaServer } from 'routing-controllers';
import mount from 'koa-mount';
import { createKoaServer } from 'routing-controllers';
import constant from './const'; import constant from './const';
import { config } from './config'; import { config } from './config';
import controllers from './controllers'; import { render } from './render';
import { router } from './router';
import { getUserByMisshaiToken } from './functions/users';
import 'reflect-metadata'; import 'reflect-metadata';
export default (): void => { export default (): void => {
const app = createKoaServer({ const app = new Koa();
controllers,
routePrefix: '/api/v1',
});
console.log('Misshaialert v' + constant.version); console.log('Misshaialert v' + constant.version);
console.log('Initializing DB connection...'); console.log('Initializing DB connection...');
app.use(render);
app.use(bodyParser()); app.use(bodyParser());
app.use(mount('/assets', serve(__dirname + '/../assets')));
useKoaServer(app, {
controllers: [ __dirname + '/controllers/**/*{.ts,.js}' ],
routePrefix: '/api/v1',
defaultErrorHandler: false,
currentUserChecker: async ({ request }: Action) => {
const authorization: string | null = request.headers['Authorization'];
if (!authorization || !authorization.startsWith('Bearer ')) return null;
const token = authorization.split(' ')[1];
return getUserByMisshaiToken(token);
},
});
app.use(router.routes());
app.keys = [ '人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...' ]; app.keys = [ '人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...' ];

View file

@ -30,7 +30,7 @@ export default (): void => {
if (user.alertMode === 'note') if (user.alertMode === 'note')
await delay(3000); await delay(3000);
} catch (e) { } catch (e: any) {
if (e.code) { if (e.code) {
if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') { if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') {
// ユーザーが削除されている場合、レコードからも消してとりやめ // ユーザーが削除されている場合、レコードからも消してとりやめ

View file

@ -3,6 +3,7 @@ doctype html
html html
head head
meta(charset="UTF-8") meta(charset="UTF-8")
link(href='https://unpkg.com/sanitize.css' rel='stylesheet')
meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(name="viewport", content="width=device-width, initial-scale=1.0")
block meta block meta
- const title = 'みす廃アラート' - const title = 'みす廃アラート'
@ -15,6 +16,19 @@ 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='/assets/style.css')
block style
body body
#app .background
script(src=`/assets/frontend.${version}.js`) .xd-container.xd-vstack
block content
footer
.xd-card
a(href="/terms") 利用規約
| ・
+exta(href="https://github.com/Xeltica/misshaialert") リポジトリ
p (C)2020-2021 Xeltica -
a(href="/about") version #{version}
block footer
block script
script(defer src='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/js/all.min.js')

31
src/views/frontend.pug Normal file
View file

@ -0,0 +1,31 @@
doctype html
html
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
block meta
- const title = 'みす廃アラート'
- const desc = '✨Misskey での1日のート数、フォロー数、フォロワー数をカウントし、深夜0時にお知らせする便利サービスです。';
title= title
meta(name='description' content=desc)
meta(property='og:title' content=title)
meta(property='og:description' content=desc)
meta(property='og:type' content='website')
meta(name='twitter:card' content='summary')
meta(name='twitter:site' content='@Xeltica')
meta(name='twitter:creator' content='@Xeltica')
style.
.loading {
display: flex;
position: fixed;
inset: 0;
background: #222;
color: #fff;
font-size: 16px;
align-items: center;
justify-content: center;
}
body
#app: .loading Loading...
script(src=`/assets/fe.${version}.js`)

300
styles/_colors.scss Normal file
View file

@ -0,0 +1,300 @@
/*
https://github.com/shuhei/material-colors
ISC License
Copyright 2014 Shuhei Kagawa
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
$md-red-50: #ffebee;
$md-red-100: #ffcdd2;
$md-red-200: #ef9a9a;
$md-red-300: #e57373;
$md-red-400: #ef5350;
$md-red-500: #f44336;
$md-red-600: #e53935;
$md-red-700: #d32f2f;
$md-red-800: #c62828;
$md-red-900: #b71c1c;
$md-red-a100: #ff8a80;
$md-red-a200: #ff5252;
$md-red-a400: #ff1744;
$md-red-a700: #d50000;
$md-pink-50: #fce4ec;
$md-pink-100: #f8bbd0;
$md-pink-200: #f48fb1;
$md-pink-300: #f06292;
$md-pink-400: #ec407a;
$md-pink-500: #e91e63;
$md-pink-600: #d81b60;
$md-pink-700: #c2185b;
$md-pink-800: #ad1457;
$md-pink-900: #880e4f;
$md-pink-a100: #ff80ab;
$md-pink-a200: #ff4081;
$md-pink-a400: #f50057;
$md-pink-a700: #c51162;
$md-purple-50: #f3e5f5;
$md-purple-100: #e1bee7;
$md-purple-200: #ce93d8;
$md-purple-300: #ba68c8;
$md-purple-400: #ab47bc;
$md-purple-500: #9c27b0;
$md-purple-600: #8e24aa;
$md-purple-700: #7b1fa2;
$md-purple-800: #6a1b9a;
$md-purple-900: #4a148c;
$md-purple-a100: #ea80fc;
$md-purple-a200: #e040fb;
$md-purple-a400: #d500f9;
$md-purple-a700: #aa00ff;
$md-deep-purple-50: #ede7f6;
$md-deep-purple-100: #d1c4e9;
$md-deep-purple-200: #b39ddb;
$md-deep-purple-300: #9575cd;
$md-deep-purple-400: #7e57c2;
$md-deep-purple-500: #673ab7;
$md-deep-purple-600: #5e35b1;
$md-deep-purple-700: #512da8;
$md-deep-purple-800: #4527a0;
$md-deep-purple-900: #311b92;
$md-deep-purple-a100: #b388ff;
$md-deep-purple-a200: #7c4dff;
$md-deep-purple-a400: #651fff;
$md-deep-purple-a700: #6200ea;
$md-indigo-50: #e8eaf6;
$md-indigo-100: #c5cae9;
$md-indigo-200: #9fa8da;
$md-indigo-300: #7986cb;
$md-indigo-400: #5c6bc0;
$md-indigo-500: #3f51b5;
$md-indigo-600: #3949ab;
$md-indigo-700: #303f9f;
$md-indigo-800: #283593;
$md-indigo-900: #1a237e;
$md-indigo-a100: #8c9eff;
$md-indigo-a200: #536dfe;
$md-indigo-a400: #3d5afe;
$md-indigo-a700: #304ffe;
$md-blue-50: #e3f2fd;
$md-blue-100: #bbdefb;
$md-blue-200: #90caf9;
$md-blue-300: #64b5f6;
$md-blue-400: #42a5f5;
$md-blue-500: #2196f3;
$md-blue-600: #1e88e5;
$md-blue-700: #1976d2;
$md-blue-800: #1565c0;
$md-blue-900: #0d47a1;
$md-blue-a100: #82b1ff;
$md-blue-a200: #448aff;
$md-blue-a400: #2979ff;
$md-blue-a700: #2962ff;
$md-light-blue-50: #e1f5fe;
$md-light-blue-100: #b3e5fc;
$md-light-blue-200: #81d4fa;
$md-light-blue-300: #4fc3f7;
$md-light-blue-400: #29b6f6;
$md-light-blue-500: #03a9f4;
$md-light-blue-600: #039be5;
$md-light-blue-700: #0288d1;
$md-light-blue-800: #0277bd;
$md-light-blue-900: #01579b;
$md-light-blue-a100: #80d8ff;
$md-light-blue-a200: #40c4ff;
$md-light-blue-a400: #00b0ff;
$md-light-blue-a700: #0091ea;
$md-cyan-50: #e0f7fa;
$md-cyan-100: #b2ebf2;
$md-cyan-200: #80deea;
$md-cyan-300: #4dd0e1;
$md-cyan-400: #26c6da;
$md-cyan-500: #00bcd4;
$md-cyan-600: #00acc1;
$md-cyan-700: #0097a7;
$md-cyan-800: #00838f;
$md-cyan-900: #006064;
$md-cyan-a100: #84ffff;
$md-cyan-a200: #18ffff;
$md-cyan-a400: #00e5ff;
$md-cyan-a700: #00b8d4;
$md-teal-50: #e0f2f1;
$md-teal-100: #b2dfdb;
$md-teal-200: #80cbc4;
$md-teal-300: #4db6ac;
$md-teal-400: #26a69a;
$md-teal-500: #009688;
$md-teal-600: #00897b;
$md-teal-700: #00796b;
$md-teal-800: #00695c;
$md-teal-900: #004d40;
$md-teal-a100: #a7ffeb;
$md-teal-a200: #64ffda;
$md-teal-a400: #1de9b6;
$md-teal-a700: #00bfa5;
$md-green-50: #e8f5e9;
$md-green-100: #c8e6c9;
$md-green-200: #a5d6a7;
$md-green-300: #81c784;
$md-green-400: #66bb6a;
$md-green-500: #4caf50;
$md-green-600: #43a047;
$md-green-700: #388e3c;
$md-green-800: #2e7d32;
$md-green-900: #1b5e20;
$md-green-a100: #b9f6ca;
$md-green-a200: #69f0ae;
$md-green-a400: #00e676;
$md-green-a700: #00c853;
$md-light-green-50: #f1f8e9;
$md-light-green-100: #dcedc8;
$md-light-green-200: #c5e1a5;
$md-light-green-300: #aed581;
$md-light-green-400: #9ccc65;
$md-light-green-500: #8bc34a;
$md-light-green-600: #7cb342;
$md-light-green-700: #689f38;
$md-light-green-800: #558b2f;
$md-light-green-900: #33691e;
$md-light-green-a100: #ccff90;
$md-light-green-a200: #b2ff59;
$md-light-green-a400: #76ff03;
$md-light-green-a700: #64dd17;
$md-lime-50: #f9fbe7;
$md-lime-100: #f0f4c3;
$md-lime-200: #e6ee9c;
$md-lime-300: #dce775;
$md-lime-400: #d4e157;
$md-lime-500: #cddc39;
$md-lime-600: #c0ca33;
$md-lime-700: #afb42b;
$md-lime-800: #9e9d24;
$md-lime-900: #827717;
$md-lime-a100: #f4ff81;
$md-lime-a200: #eeff41;
$md-lime-a400: #c6ff00;
$md-lime-a700: #aeea00;
$md-yellow-50: #fffde7;
$md-yellow-100: #fff9c4;
$md-yellow-200: #fff59d;
$md-yellow-300: #fff176;
$md-yellow-400: #ffee58;
$md-yellow-500: #ffeb3b;
$md-yellow-600: #fdd835;
$md-yellow-700: #fbc02d;
$md-yellow-800: #f9a825;
$md-yellow-900: #f57f17;
$md-yellow-a100: #ffff8d;
$md-yellow-a200: #ffff00;
$md-yellow-a400: #ffea00;
$md-yellow-a700: #ffd600;
$md-amber-50: #fff8e1;
$md-amber-100: #ffecb3;
$md-amber-200: #ffe082;
$md-amber-300: #ffd54f;
$md-amber-400: #ffca28;
$md-amber-500: #ffc107;
$md-amber-600: #ffb300;
$md-amber-700: #ffa000;
$md-amber-800: #ff8f00;
$md-amber-900: #ff6f00;
$md-amber-a100: #ffe57f;
$md-amber-a200: #ffd740;
$md-amber-a400: #ffc400;
$md-amber-a700: #ffab00;
$md-orange-50: #fff3e0;
$md-orange-100: #ffe0b2;
$md-orange-200: #ffcc80;
$md-orange-300: #ffb74d;
$md-orange-400: #ffa726;
$md-orange-500: #ff9800;
$md-orange-600: #fb8c00;
$md-orange-700: #f57c00;
$md-orange-800: #ef6c00;
$md-orange-900: #e65100;
$md-orange-a100: #ffd180;
$md-orange-a200: #ffab40;
$md-orange-a400: #ff9100;
$md-orange-a700: #ff6d00;
$md-deep-orange-50: #fbe9e7;
$md-deep-orange-100: #ffccbc;
$md-deep-orange-200: #ffab91;
$md-deep-orange-300: #ff8a65;
$md-deep-orange-400: #ff7043;
$md-deep-orange-500: #ff5722;
$md-deep-orange-600: #f4511e;
$md-deep-orange-700: #e64a19;
$md-deep-orange-800: #d84315;
$md-deep-orange-900: #bf360c;
$md-deep-orange-a100: #ff9e80;
$md-deep-orange-a200: #ff6e40;
$md-deep-orange-a400: #ff3d00;
$md-deep-orange-a700: #dd2c00;
$md-brown-50: #efebe9;
$md-brown-100: #d7ccc8;
$md-brown-200: #bcaaa4;
$md-brown-300: #a1887f;
$md-brown-400: #8d6e63;
$md-brown-500: #795548;
$md-brown-600: #6d4c41;
$md-brown-700: #5d4037;
$md-brown-800: #4e342e;
$md-brown-900: #3e2723;
$md-grey-50: #fafafa;
$md-grey-100: #f5f5f5;
$md-grey-200: #eeeeee;
$md-grey-300: #e0e0e0;
$md-grey-400: #bdbdbd;
$md-grey-500: #9e9e9e;
$md-grey-600: #757575;
$md-grey-700: #616161;
$md-grey-800: #424242;
$md-grey-900: #212121;
$md-blue-grey-50: #eceff1;
$md-blue-grey-100: #cfd8dc;
$md-blue-grey-200: #b0bec5;
$md-blue-grey-300: #90a4ae;
$md-blue-grey-400: #78909c;
$md-blue-grey-500: #607d8b;
$md-blue-grey-600: #546e7a;
$md-blue-grey-700: #455a64;
$md-blue-grey-800: #37474f;
$md-blue-grey-900: #263238;
$md-black: #000000;
$md-white: #ffffff;
$md-dark-text-primary: rgba(0, 0, 0, 0.87);
$md-dark-text-secondary: rgba(0, 0, 0, 0.54);
$md-dark-text-disabled: rgba(0, 0, 0, 0.38);
$md-dark-text-dividers: rgba(0, 0, 0, 0.12);
$md-light-text-primary: rgba(255, 255, 255, 1);
$md-light-text-secondary: rgba(255, 255, 255, 0.7);
$md-light-text-disabled: rgba(255, 255, 255, 0.5);
$md-light-text-dividers: rgba(255, 255, 255, 0.12);
$md-dark-icons-active: rgba(0, 0, 0, 0.54);
$md-dark-icons-inactive: rgba(0, 0, 0, 0.38);
$md-light-icons-active: rgba(255, 255, 255, 1);
$md-light-icons-inactive: rgba(255, 255, 255, 0.5);

501
styles/_xeltica-design.scss Normal file
View file

@ -0,0 +1,501 @@
/*
Xeltica Design CSS Framework
(C)2020 Xeltica
*/
* {
box-sizing: border-box;
line-height: 1.8em;
}
html {
$primary: rgb(134, 179, 0);
$fg: rgba(255, 255, 255, 0.8);
$bg: rgba(24, 24, 24, 0.8);
$overlay: transparentize($bg, 0.5);
$danger: #c72c2c;
font-size: 18px;
@media screen and (max-width: 640px) {
font-size: 15px;
}
--primary: #{$primary};
--primary-light: #{lighten($primary, 5%)};
--primary-dark: #{darken($primary, 5%)};
--primary-fg: white;
--bg: #{$bg};
--bg-pale-1: #{darken($bg, 5%)};
--bg-pale-2: #{darken($bg, 10%)};
--overlay: #{$overlay};
--fg: #{$fg};
// --divider: rgba(70, 70, 70, 0.25);
--radius: 0px;
--margin: 16px;
--bg-danger: #{$danger};
--fg-danger: white;
--divider-danger: #400e0e;
--bg-danger-lighten: #{lighten($danger, 5%)};
--bg-danger-darken: #{darken($danger, 5%)};
}
body {
background: var(--bg);
color: var(--fg);
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
margin: 0;
margin-bottom: 8px;
}
dl {
margin: 0;
> dt {
font-weight: bold;
}
> dd {
margin-left: 2rem;
@media screen and (max-width: 640px) {
margin-left: 0;
}
}
}
h1 { font-size: 2.5rem; }
h2 { font-size: 1.6rem; }
h3 { font-size: 1.4rem; }
h4 { font-size: 1.2rem; }
h5 { font-size: 1.1rem; }
h6 { font-size: 1.05rem; }
input.xd-input {
outline: none;
border-radius: var(--radius);
color: var(--fg);
background: transparent;
border: 1px solid var(--fg);
padding: 2px 8px;
font-size: 1rem;
&:focus {
border-color: var(--primary);
}
}
p {
margin: 1em 0;
&:first-child {
margin-top: 0;
}
}
.xd-inputs {
display: flex;
> *:not(:first-child) {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
> *:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.xd-main {
background: var(--bg);
padding: 32px;
border-radius: var(--radius);
border: 1px solid var(--divider);
max-width: 960px;
margin: 24px auto;
backdrop-filter: blur(32px) saturate(4);
-webkit-backdrop-filter: blur(32px) saturate(4);
box-shadow: 0 0 16px black;
}
button, .xd-button {
display: flex;
color: var(--fg);
background: var(--bg);
border: 1px solid var(--bg-pale-1);
align-items: center;
justify-content: center;
border-radius: var(--radius);
font-size: 1rem;
outline: none;
padding: 2px 8px;
text-decoration: none;
cursor: pointer;
&:hover, &:focus {
background: var(--bg-pale-1);
}
&:active {
background: var(--bg-pale-2);
}
&.primary {
background: var(--primary);
color: var(--primary-fg);
border-color: var(--primary-dark);
&:hover, &:focus {
background: var(--primary-light);
}
&:active {
background: var(--primary-dark);
}
}
&.danger {
background: var(--bg-danger);
color: white;
border-color: var(--divider-danger);
&:hover, &:focus {
background: var(--bg-danger-lighten);
}
&:active {
background: var(--bg-danger-lighten);
}
}
}
textarea {
width: 100%;
font-size: 1rem;
padding: 16px;
border-radius: var(--radius);
background: var(--overlay);
border: none;
outline: none;
height: 8rem;
line-height: 1.2;
color: var(--fg);
&:focus {
border: 1px solid var(--primary);
}
}
// ul, ol {
// > ul, > ol {
// margin-bottom: 0;
// }
// }
a, .link {
text-decoration: none;
color: var(--primary);
&:hover {
opacity: 0.7;
}
}
table {
border: 1px solid var(--divider);
border-radius: var(--radius);
background: var(--overlay);
width: 100%;
> thead {
background: var(--overlay);
margin-bottom: 2px;
text-align: left;
}
> tbody > tr {
border-bottom: 1px solid var(--overlay);
}
th, td {
padding: 4px 8px;
}
}
code {
border-radius: var(--radius);
color: #0f0;
background: #000;
padding: 8px;
}
.xd-container {
max-width: 960px;
margin: 64px auto;
}
.xd-hstack {
display: flex;
> *:not(:last-child) {
margin-right: var(--margin);
}
> * {
width: 100%;
}
@media screen and (max-width: 640px) {
flex-direction: column;
> * {
margin-bottom: var(--margin);
}
}
}
.xd-vstack {
display: flex;
flex-direction: column;
> *:not(:last-child) {
margin-bottom: var(--margin);
}
}
.xd-card {
background: var(--bg);
padding: 16px;
border-radius: var(--radius);
border: 1px solid var(--divider);
// backdrop-filter: blur(32px) saturate(4);
// -webkit-backdrop-filter: blur(32px) saturate(4);
box-shadow: 0 0 16px black;
overflow: hidden;
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.3rem; }
h4 { font-size: 1.17rem; }
h5 { font-size: 1.12rem; }
h6 { font-size: 1.08rem; }
> .media {
width: 100%;
height: auto;
object-fit: cover;
}
> .body {
padding: 16px;
}
> .header, > .footer {
padding: 8px 16px;
}
> .header {
> h1.title {
font-size: 1.5rem;
margin: 0;
font-weight: bold;
}
}
> .footer {
background: var(--bg-pale-1);
border-top: 1px solid var(--divider);
}
@media screen and (max-width: 640px) {
border-radius: 0;
}
}
.xd-cards {
list-style: none;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: left;
> .xd-card {
width: 100%;
margin: 8px;
}
&.center {
justify-content: center;
}
@media screen and (max-width: 640px) {
flex-wrap: wrap;
> .xd-card {
margin: 8px 0;
}
}
&.wrap {
flex-wrap: wrap;
}
}
.mx-auto { margin-left: auto; margin-right: auto; }
.my-auto { margin-top: auto; margin-bottom: auto; }
.px-auto { padding-left: auto; padding-right: auto; }
.py-auto { padding-top: auto; padding-bottom: auto; }
@for $v from -5 through 5 {
$size: $v * 8px;
.ma-#{$v} { margin: $size; }
.ml-#{$v} { margin-left: $size; }
.mr-#{$v} { margin-right: $size; }
.mt-#{$v} { margin-top: $size; }
.mb-#{$v} { margin-bottom: $size; }
.mx-#{$v} { margin-left: $size; margin-right: $size; }
.my-#{$v} { margin-top: $size; margin-bottom: $size; }
.pa-#{$v} { padding: $size; }
.pl-#{$v} { padding-left: $size; }
.pr-#{$v} { padding-right: $size; }
.pt-#{$v} { padding-top: $size; }
.pb-#{$v} { padding-bottom: $size; }
.px-#{$v} { padding-left: $size; padding-right: $size; }
.py-#{$v} { padding-top: $size; padding-bottom: $size; }
}
.xd-slide-in {
animation: slideIn 1s ease-out;
}
@keyframes slideIn {
0% {
transform: translateY(32px);
opacity: 0;
}
100% {
transform: none;
opacity: 1;
}
}
figure {
margin: 0;
padding: 0;
}
@each $mode in none, block, inline, flex, grid, inline-block, inline-flex {
.display-#{$mode} { display: $mode; }
}
@media screen and (min-width: 901px) {
.hide-on-pc {
display: none !important;
}
}
@media screen and (max-width: 900px) and (min-width: 641px) {
.hide-on-tablet {
display: none !important;
}
}
@media screen and (max-width: 640px) {
.hide-on-mobile {
display: none !important;
}
}
@for $i from -18 through 18 {
.xd-tilt-#{$i * 5} {
display: inline-block;
transform: rotateZ($i * 5deg);
}
}
@keyframes blink {
0% { opacity: 1 }
10% { opacity: 1 }
12% { opacity: 0 }
14% { opacity: 1 }
20% { opacity: 1 }
21% { opacity: 0 }
39% { opacity: 0 }
40% { opacity: 1 }
48% { opacity: 1 }
49% { opacity: 0 }
50% { opacity: 1 }
54% { opacity: 1 }
55% { opacity: 0 }
56% { opacity: 1 }
85% { opacity: 1 }
89% { opacity: 0 }
95% { opacity: 1 }
}
.xd-big {
font-size: 2rem;
}
.xd-small {
font-size: 0.5rem;
}
.xd-blink {
animation: blink 2s infinite linear;
}
.xd-fluid {
width: 100%;
}
img, .xd-responsive {
max-width: 100%;
}
.xd-footer {
text-align: center;
// background: darken($bg, 2);
}
.xd-columns {
display: flex;
@media screen and (max-width: 640px) {
flex-wrap: wrap;
}
.xd-column {
&.gap-1 { margin: 8px; }
&.gap-2 { margin: 16px; }
&.gap-3 { margin: 24px; }
}
}
.xd-alert {
padding: 8px 16px;
display: flex;
align-items: center;
border-radius: var(--radius);
width: 100%;
background: var(--bg);
border: 1px solid var(--bg-pale-2);
color: var(--fg);
font-size: 75%;
> .icon {
opacity: 0.5;
font-size: 1.2rem;
margin-right: 16px;
}
&.danger {
> .icon {
opacity: 1;
}
background: var(--bg-danger);
border: 1px solid var(--divider-danger);
color: var(--fg-danger);
}
}
.xd-center {
text-align: center;
}
.xd-centerized {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 100vw;
height: 100vh;
}

58
styles/style.scss Normal file
View file

@ -0,0 +1,58 @@
@import '_xeltica-design.scss';
body {
background: var(--bg-pale-1);
}
h1> a {
border-bottom: none;
}
.background {
position: fixed;
z-index: -50;
inset: 0;
background: radial-gradient(
farthest-corner at 0 0,
#86b300 0%,
#86b300 40%,
#6900ba 90%,
#6900ba 100%
);
}
.background:before {
z-index: -200;
position: absolute;
inset: 0;
background: radial-gradient(
60vw at 150vh 50vh,
#ff9900 0%,
#ff9900 20%,
rgba(0, 0, 0, 0) 100%
);
content: "";
}
.background:after {
z-index: -100;
position: absolute;
inset: 0;
background: radial-gradient(
50vw at 120vw 120vh,
rgba(0, 30, 190, 1) 0%,
rgba(0, 30, 190, 0) 100%
);
content: "";
}
details > summary {
margin-bottom: 8px;
cursor: pointer;
}
h1 > small {
margin-left: 1rem;
font-size: 1.2rem;
}

View file

@ -1,77 +1,23 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./built/", /* Redirect output structure to the directory. */ "outDir": "./built/", /* Redirect output structure to the directory. */
"rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [ "typeRoots": [
"node_modules/@types", "node_modules/@types",
"src/@types" "src/@types"
], /* List of folders to include type definitions from. */ ], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */ "skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}, },
"exclude": [ "exclude": [
"./migration" "./migration",
"./src/frontend"
] ]
} }

92
webpack.config.js Normal file
View file

@ -0,0 +1,92 @@
/**
* webpack configuration
*/
const fs = require('fs');
const webpack = require('webpack');
// class WebpackOnBuildPlugin {
// constructor(readonly callback: (stats: any) => void) {
// }
// public apply(compiler: any) {
// compiler.hooks.done.tap('WebpackOnBuildPlugin', this.callback);
// }
// }
const isProduction = process.env.NODE_ENV === 'production';
const meta = require('./package.json');
module.exports = {
entry: {
fe: './src/frontend/init.tsx',
},
module: {
rules: [{
test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/,
type: 'asset/resource'
}, {
test: /\.json5$/,
loader: 'json5-loader',
options: {
esModule: false,
},
type: 'javascript/auto'
}, {
test: /\.tsx?$/,
use: [
{ loader: 'ts-loader' }
]
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
url: false,
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: {
fiber: false
},
sourceMap: true,
},
},
],
}, {
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}]
},
plugins: [
new webpack.ProgressPlugin({}),
],
output: {
path: __dirname + '/built/assets',
filename: `[name].${meta.version}.js`,
publicPath: '/assets/',
pathinfo: false,
},
resolve: {
extensions: [
'.js', '.ts', '.json', '.tsx'
],
alias: {
}
},
resolveLoader: {
modules: ['node_modules']
},
experiments: {
topLevelAwait: true
},
devtool: false, //'source-map',
mode: isProduction ? 'production' : 'development'
};

2011
yarn.lock

File diff suppressed because it is too large Load diff