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': {
'indent': [
'error',
'tab'
'tab',
],
'linebreak-style': [
'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"],
"ext": "ts",
"ext": "ts,tsx,pug,scss",
"exec": "run-s build start"
}

View File

@ -1,70 +1,95 @@
{
"name": "misshaialert",
"version": "1.5.1",
"description": "",
"main": "built/app.js",
"author": "Xeltica",
"private": true,
"scripts": {
"tsc": "tsc",
"start": "node built/app.js",
"lint": "eslint src/index.ts",
"lint:fix": "eslint --fix src/index.ts",
"clean": "rimraf built",
"build:backend": "tsc",
"build": "run-p build:*",
"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",
"dev": "nodemon"
},
"dependencies": {
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-mount": "^4.0.0",
"@types/koa-multer": "^1.0.0",
"@types/koa-static": "^4.0.1",
"@types/node-cron": "^2.0.3",
"@types/uuid": "^8.0.0",
"axios": "^0.19.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"dayjs": "^1.10.2",
"delay": "^4.4.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"koa-mount": "^4.0.0",
"koa-multer": "^1.0.2",
"koa-router": "^9.1.0",
"koa-session": "^6.0.0",
"koa-static": "^5.0.0",
"koa-views": "^6.3.0",
"node-cron": "^2.0.3",
"pg": "^8.3.0",
"pug": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rndstr": "^1.0.0",
"routing-controllers": "^0.9.0",
"sass": "^1.26.10",
"typeorm": "0.2.25",
"typescript": "^3.9.7",
"uuid": "^8.3.0"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/koa": "^2.11.3",
"@types/koa-router": "^7.4.1",
"@types/koa-session": "^5.10.2",
"@types/koa-views": "^2.0.4",
"@types/node": "^8.0.29",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"copyfiles": "^2.3.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
"ts-node": "3.3.0"
}
"name": "misshaialert",
"version": "1.5.1",
"description": "",
"main": "built/app.js",
"author": "Xeltica",
"private": true,
"scripts": {
"tsc": "tsc",
"start": "node built/app.js",
"lint": "eslint --ext .ts,.tsx src",
"lint:fix": "eslint --fix --ext .ts,.tsx src",
"clean": "rimraf built",
"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:*",
"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",
"dev": "nodemon",
"dev:frontend": "webpack --watch"
},
"dependencies": {
"@babel/preset-react": "^7.14.5",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-multer": "^1.0.0",
"@types/koa-send": "^4.1.3",
"@types/ms": "^0.7.31",
"@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",
"axios": "^0.19.2",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"css-loader": "^6.2.0",
"dayjs": "^1.10.2",
"delay": "^4.4.0",
"fibers": "^5.0.0",
"json5-loader": "^4.0.1",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"koa-multer": "^1.0.2",
"koa-router": "^9.1.0",
"koa-send": "^5.0.1",
"koa-session": "^6.0.0",
"koa-views": "^6.3.0",
"misskey-js": "^0.0.6",
"ms": "^2.1.3",
"node-cron": "^2.0.3",
"object.pick": "^1.3.0",
"pg": "^8.3.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",
"rndstr": "^1.0.0",
"routing-controllers": "^0.9.0",
"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",
"typescript": "^4.4.2",
"uuid": "^8.3.0",
"webpack": "^5.51.1",
"webpack-cli": "^4.8.0",
"xeltica-ui": "xeltica/ui"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/koa": "^2.11.3",
"@types/koa-router": "^7.4.1",
"@types/koa-session": "^5.10.2",
"@types/koa-views": "^2.0.4",
"@types/node": "^8.0.29",
"@typescript-eslint/eslint-plugin": "^4.30.0",
"@typescript-eslint/parser": "^4.30.0",
"copyfiles": "^2.3.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"nodemon": "^2.0.4",
"npm-run-all": "^4.1.5",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
"ts-node": "3.3.0"
}
}

View File

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

View File

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

View File

@ -1,21 +1,31 @@
import { Get, JsonController, QueryParam } from "routing-controllers";
import { getRanking } from "../functions/ranking";
import { getState } from "../store";
/**
* API
* @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 {
@Get('/ranking')
get(
@QueryParam('limit', { type: Number, required: false })
limit?: number
) {
return this.getResponse(getState().nowCalculating)
}
@Get()
async get(@QueryParam('limit', { required: false }) limit?: string) {
return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
}
private getResponse(isCalculating: boolean, limit?: number) {
return {
isCalculating,
ranking: isCalculating ? [] : getRanking(limit),
};
}
}
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 {
isCalculating,
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 { DeepPartial } from 'typeorm';
import { genToken } from './gen-token';
import pick from 'object.pick';
export const getUser = (username: string, host: string): Promise<User | undefined> => {
return Users.findOne({ username, host });
@ -40,4 +41,4 @@ export const deleteUser = async (username: string, host: string): Promise<void>
export const getUserCount = (): Promise<number> => {
return Users.count();
};
};

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,91 +2,29 @@ import { Context, DefaultState } from 'koa';
import Router from 'koa-router';
import axios from 'axios';
import crypto from 'crypto';
import koaSend from 'koa-send';
import { v4 as uuid } from 'uuid';
import { config } from '../config';
import { upsertUser, getUser, getUserCount, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from '../functions/users';
import { api, apiAvailable } from '../services/misskey';
import { getScores } from '../functions/get-scores';
import { AlertMode, alertModes } from '../types/AlertMode';
import { Users } from '../models';
import { send } from '../services/send';
import { visibilities, Visibility } from '../types/Visibility';
import { defaultTemplate, variables } from '../functions/format';
import { getRanking } from '../functions/ranking';
import { getState } from '../store';
import { welcomeMessage } from '../misc/welcome-message';
import ms from 'ms';
import { config } from './config';
import { upsertUser, getUser, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from './functions/users';
import { api } from './services/misskey';
import { AlertMode, alertModes } from './types/alert-mode';
import { Users } from './models';
import { send } from './services/send';
import { visibilities, Visibility } from './types/visibility';
import { defaultTemplate } from './functions/format';
import { die } from './die';
export const router = new Router<DefaultState, Context>();
const sessionHostCache: 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 => {
let host = ctx.query.host as string | undefined;
if (!host) {
if (!host) {
await die(ctx, 'host is empty');
return;
}
@ -110,7 +48,7 @@ router.get('/login', async ctx => {
const session = uuid();
const url = `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`;
sessionHostCache[session] = host;
ctx.redirect(url);
} else {
// Use legacy authentication
@ -135,13 +73,6 @@ router.get('/teapot', async ctx => {
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 => {
const session = ctx.query.session as string | undefined;
if (!session) {
@ -164,7 +95,7 @@ router.get('/miauth', async ctx => {
}
await login(ctx, user, host, token);
});
router.get('/legacy-auth', async ctx => {
@ -215,7 +146,7 @@ router.post('/update-settings', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
return;
}
const u = await getUserByMisshaiToken(token);
@ -242,7 +173,7 @@ router.post('/logout', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
return;
}
ctx.cookies.set('token', '');
ctx.redirect('/?from=logout');
@ -252,7 +183,7 @@ router.post('/optout', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
return;
}
ctx.cookies.set('token', '');
@ -264,7 +195,7 @@ router.post('/optout', async ctx => {
}
await deleteUser(u.username, u.host);
ctx.redirect('/?from=optout');
});
@ -272,9 +203,9 @@ router.post('/send', async ctx => {
const token = ctx.cookies.get('token');
if (!token) {
await die(ctx, 'ログインしていません');
return;
return;
}
const u = await getUserByMisshaiToken(token);
if (!u) {
@ -285,8 +216,44 @@ router.post('/send', async ctx => {
ctx.redirect('/?from=send');
});
router.get('/assets/(.*)', async ctx => {
await koaSend(ctx as any, ctx.path.replace('/assets/', ''), {
root: `${__dirname}/assets/`,
maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'),
});
});
// Return 404 for other pages
router.all('(.*)', async ctx => {
await die(ctx, 'ページが見つかりませんでした', 404);
});
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 serve from 'koa-static';
import mount from 'koa-mount';
import { createKoaServer } from 'routing-controllers';
import { Action, useKoaServer } from 'routing-controllers';
import constant from './const';
import { config } from './config';
import controllers from './controllers';
import { render } from './render';
import { router } from './router';
import { getUserByMisshaiToken } from './functions/users';
import 'reflect-metadata';
export default (): void => {
const app = createKoaServer({
controllers,
routePrefix: '/api/v1',
});
const app = new Koa();
console.log('Misshaialert v' + constant.version);
console.log('Initializing DB connection...');
app.use(render);
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 = [ '人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...' ];

View File

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

View File

@ -1,20 +1,34 @@
include _components
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')
body
#app
script(src=`/assets/frontend.${version}.js`)
head
meta(charset="UTF-8")
link(href='https://unpkg.com/sanitize.css' rel='stylesheet')
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')
link(rel='stylesheet' href='/assets/style.css')
block style
body
.background
.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')

View File

@ -1,40 +1,40 @@
mixin exta()
a(href=attributes.href target="_blank" rel="noopener noreferrer")
block
a(href=attributes.href target="_blank" rel="noopener noreferrer")
block
mixin ranking()
.xd-card
.header
h1.title みす廃ランキング
.body
p
i.fas.fa-users
strong 登録者数: !{usersCount}人
if state && state.nowCalculating
p 現在計算中です。後ほどご確認ください
else
+rankingTable()
p: a(href="/ranking") 全員分見る
.xd-card
.header
h1.title みす廃ランキング
.body
p
i.fas.fa-users
strong 登録者数: !{usersCount}人
if state && state.nowCalculating
p 現在計算中です。後ほどご確認ください
else
+rankingTable()
p: a(href="/ranking") 全員分見る
mixin rankingTable()
table
thead: tr
th 順位
th ユーザー
th レート
tbody
-
let rank = 1;
let lastRating = '';
each rec in ranking
- const rating = rec.rating.toFixed(2);
tr
td=rank
td: +exta(href="https://" + rec.host + "/@" + rec.username) @!{rec.username}<wbr/>@!{rec.host}
td=rating
-
if (lastRating !== rating) {
rank++;
}
lastRating = rating
table
thead: tr
th 順位
th ユーザー
th レート
tbody
-
let rank = 1;
let lastRating = '';
each rec in ranking
- const rating = rec.rating.toFixed(2);
tr
td=rank
td: +exta(href="https://" + rec.host + "/@" + rec.username) @!{rec.username}<wbr/>@!{rec.host}
td=rating
-
if (lastRating !== rating) {
rank++;
}
lastRating = rating

View File

@ -1,10 +1,10 @@
extends _base
block content
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 バージョン !{version}
ul
each log in changelog
li= log
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 バージョン !{version}
ul
each log in changelog
li= log

View File

@ -1,7 +1,7 @@
extends _base
block content
h1: a(href="/") みす廃あらーと
section.xd-card
h2 エラー
p= error
h1: a(href="/") みす廃あらーと
section.xd-card
h2 エラー
p= error

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`)

View File

@ -1,162 +1,162 @@
extends _base
block content
.xd-card
h1: a(href="/") みす廃あらーと
h2 マイページ
p おかえりなさい、@!{ user.username }@!{ user.host } さん。
.xd-card
h1: a(href="/") みす廃あらーと
h2 マイページ
p おかえりなさい、@!{ user.username }@!{ user.host } さん。
case from
when "updateSettings"
.xd-alert.my-2
i.icon.fas.fa-thumbs-up
strong 設定を変更しました。
when "send"
.xd-alert.my-2
i.icon.fas.fa-thumbs-up
strong テスト送信しました。
if isGroundpolis
.xd-alert.my-2
i.icon.fas.fa-meteor
strong Groundpolis アカウントでログインしています。専用機能が有効化されました。
section#scores.xd-vstack
.xd-hstack
+ranking()
.xd-card
.header
h1.title みす廃データ
.body
table
thead
tr
th 内容
th スコア
th 前日比
tbody
tr
td ノート
td !{score.notesCount}
td !{score.notesDelta}
tr
td フォロー
td !{score.followingCount}
td !{score.followingDelta}
tr
td フォロワー
td !{score.followersCount}
td !{score.followersDelta}
tr
td フォロワー
td !{score.followersCount}
td !{score.followersDelta}
p みす廃レート: !{user.rating}
section.xd-card#settings
-
const visibilities = [
[ 'public', 'パブリック'],
[ 'home', isGroundpolis ? '未収載' : 'ホーム'],
[ 'followers', 'フォロワー'],
];
if (isGroundpolis) visibilities.push(['users', 'ログインユーザー']);
const alertModes = [
[ 'note', '自動的にノートを投稿' ],
[ 'notification', 'Misskeyに通知(標準)' ],
[ 'nothing', '通知しない' ],
];
const currentAlertModeLabel = alertModes.find(a => a[0] === user.alertMode)[1];
.header
h1.title 設定
.body
.xd-alert.danger.mb-2
i.icon.fas.fa-exclamation-circle
| スコア通知方法に「Misskey に通知」を選んでいる場合、Groundpolis v3 および Misskey v12 の最新版以外では動作しません。めいすきーや古いバージョンをお使いの方は、「自動的にノートを投稿」をお使いください。
form(method="post", action="/update-settings")
p: label スコア通知方法:
select#alertModeSelector(name="alertMode")
each set in alertModes
option(value=set[0], selected=(user.alertMode === set[0]))= set[1]
#hideWhenAlertModeNotNote
p: label 公開範囲:
select(name="visibility")
each set in visibilities
option(value=set[0], selected=(user.visibility === set[0]))= set[1]
p
| フラグ <br />
label
input(type="radio", name="flag", value="none", checked=!user.localOnly && !user.remoteFollowersOnly)
| なし(標準)<br />
label
input(type="radio", name="flag", value="localOnly", checked=user.localOnly)
| ローカルのみ<br />
if isGroundpolis
label
input(type="radio", name="flag", value="remoteFollowersOnly", checked=user.remoteFollowersOnly)
| リモートフォロワーとローカル<br />
#hideWhenAlertModeNothing
div
label 投稿テンプレート<br/>
textarea#template(name="template", maxlength=280, placeholder=defaultTemplate)=user.template || defaultTemplate
details()
summary ヘルプ
ul
li テンプレートに使える文字数は280文字です。
li 空欄にすると、デフォルト値にリセットされます。
li ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。
li
code {notesCount}
| といった形式のテキストは変数として扱われ、これを含めると投稿時に自動的に値が埋め込まれます。
p 変数の表を以下に示します。変数をクリックすると自動挿入されます。
table
thead: tr
th 変数
th 説明
tbody
each val, key in templateVariables
tr
td(onclick=`insert('{${key}}')`, style="cursor: pointer")=key
td=val.description
button.primary(type="submit") 保存
case from
when "updateSettings"
.xd-alert.my-2
i.icon.fas.fa-thumbs-up
strong 設定を変更しました。
when "send"
.xd-alert.my-2
i.icon.fas.fa-thumbs-up
strong テスト送信しました。
if isGroundpolis
.xd-alert.my-2
i.icon.fas.fa-meteor
strong Groundpolis アカウントでログインしています。専用機能が有効化されました。
section#scores.xd-vstack
.xd-hstack
+ranking()
.xd-card
.header
h1.title みす廃データ
.body
table
thead
tr
th 内容
th スコア
th 前日比
tbody
tr
td ノート
td !{score.notesCount}
td !{score.notesDelta}
tr
td フォロー
td !{score.followingCount}
td !{score.followingDelta}
tr
td フォロワー
td !{score.followersCount}
td !{score.followersDelta}
tr
td フォロワー
td !{score.followersCount}
td !{score.followersDelta}
p みす廃レート: !{user.rating}
section.xd-card#settings
-
const visibilities = [
[ 'public', 'パブリック'],
[ 'home', isGroundpolis ? '未収載' : 'ホーム'],
[ 'followers', 'フォロワー'],
];
if (isGroundpolis) visibilities.push(['users', 'ログインユーザー']);
const alertModes = [
[ 'note', '自動的にノートを投稿' ],
[ 'notification', 'Misskeyに通知(標準)' ],
[ 'nothing', '通知しない' ],
];
const currentAlertModeLabel = alertModes.find(a => a[0] === user.alertMode)[1];
.header
h1.title 設定
.body
.xd-alert.danger.mb-2
i.icon.fas.fa-exclamation-circle
| スコア通知方法に「Misskey に通知」を選んでいる場合、Groundpolis v3 および Misskey v12 の最新版以外では動作しません。めいすきーや古いバージョンをお使いの方は、「自動的にノートを投稿」をお使いください。
form(method="post", action="/update-settings")
p: label スコア通知方法:
select#alertModeSelector(name="alertMode")
each set in alertModes
option(value=set[0], selected=(user.alertMode === set[0]))= set[1]
#hideWhenAlertModeNotNote
p: label 公開範囲:
select(name="visibility")
each set in visibilities
option(value=set[0], selected=(user.visibility === set[0]))= set[1]
p
| フラグ <br />
label
input(type="radio", name="flag", value="none", checked=!user.localOnly && !user.remoteFollowersOnly)
| なし(標準)<br />
label
input(type="radio", name="flag", value="localOnly", checked=user.localOnly)
| ローカルのみ<br />
if isGroundpolis
label
input(type="radio", name="flag", value="remoteFollowersOnly", checked=user.remoteFollowersOnly)
| リモートフォロワーとローカル<br />
#hideWhenAlertModeNothing
div
label 投稿テンプレート<br/>
textarea#template(name="template", maxlength=280, placeholder=defaultTemplate)=user.template || defaultTemplate
details()
summary ヘルプ
ul
li テンプレートに使える文字数は280文字です。
li 空欄にすると、デフォルト値にリセットされます。
li ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。
li
code {notesCount}
| といった形式のテキストは変数として扱われ、これを含めると投稿時に自動的に値が埋め込まれます。
p 変数の表を以下に示します。変数をクリックすると自動挿入されます。
table
thead: tr
th 変数
th 説明
tbody
each val, key in templateVariables
tr
td(onclick=`insert('{${key}}')`, style="cursor: pointer")=key
td=val.description
button.primary(type="submit") 保存
section.xd-card#settings
.header
h1.title 操作
.body
form.mb-2(action="/send", method="post"): button#send(style="display: inline-block") アラートをテスト送信
form.mb-2(action="/logout", method="post"): button#logout(style="display: inline-block") ログアウト
form.mb-2(action="/optout", method="post"): button.danger#optout(style="display: inline-block") アカウント連携を解除する
section.xd-card#settings
.header
h1.title 操作
.body
form.mb-2(action="/send", method="post"): button#send(style="display: inline-block") アラートをテスト送信
form.mb-2(action="/logout", method="post"): button#logout(style="display: inline-block") ログアウト
form.mb-2(action="/optout", method="post"): button.danger#optout(style="display: inline-block") アカウント連携を解除する
block script
script.
history.replaceState(null, null, '/');
document.getElementById('send').addEventListener('click', (e) => {
if (!confirm('現在の設定「!{currentAlertModeLabel}」に基づいてアラートを送信しますか?'))
e.preventDefault();
});
document.getElementById('optout').addEventListener('click', (e) => {
if (!confirm('連携を解除すると、統計情報などのデータが削除されてしまい、以後アラート機能をご利用いただけなくなります。この操作は変更できません。\n\nそれでもなお、連携を解除しますか'))
e.preventDefault();
});
document.getElementById('logout').addEventListener('click', (e) => {
if (!confirm('ログアウトしますか?'))
e.preventDefault();
});
const hideWhenAlertModeNotNote = document.getElementById('hideWhenAlertModeNotNote');
const hideWhenAlertModeNothing = document.getElementById('hideWhenAlertModeNothing');
const alertModeSelector = document.getElementById('alertModeSelector');
const updateView = () => {
const value = alertModeSelector.value;
hideWhenAlertModeNotNote.style.display = value !== 'note' ? 'none' : 'block';
hideWhenAlertModeNothing.style.display = value === 'nothing' ? 'none' : 'block';
};
alertModeSelector.addEventListener('change', updateView);
updateView();
const template = document.getElementById('template');
function insert(text) {
template.value += text;
}
script.
history.replaceState(null, null, '/');
document.getElementById('send').addEventListener('click', (e) => {
if (!confirm('現在の設定「!{currentAlertModeLabel}」に基づいてアラートを送信しますか?'))
e.preventDefault();
});
document.getElementById('optout').addEventListener('click', (e) => {
if (!confirm('連携を解除すると、統計情報などのデータが削除されてしまい、以後アラート機能をご利用いただけなくなります。この操作は変更できません。\n\nそれでもなお、連携を解除しますか'))
e.preventDefault();
});
document.getElementById('logout').addEventListener('click', (e) => {
if (!confirm('ログアウトしますか?'))
e.preventDefault();
});
const hideWhenAlertModeNotNote = document.getElementById('hideWhenAlertModeNotNote');
const hideWhenAlertModeNothing = document.getElementById('hideWhenAlertModeNothing');
const alertModeSelector = document.getElementById('alertModeSelector');
const updateView = () => {
const value = alertModeSelector.value;
hideWhenAlertModeNotNote.style.display = value !== 'note' ? 'none' : 'block';
hideWhenAlertModeNothing.style.display = value === 'nothing' ? 'none' : 'block';
};
alertModeSelector.addEventListener('change', updateView);
updateView();
const template = document.getElementById('template');
function insert(text) {
template.value += text;
}

View File

@ -1,15 +1,15 @@
extends _base
block content
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 みす廃ランキング
details
summary これは何?
p みす廃ランキングは、独自に算出された「<strong>みす廃レート</strong>」の高い順ランキングです。毎日みす廃あらーとが発行される度に更新されます。
p みす廃レートは、登録日からの経過日数およびノート数から算出されます。
if state.nowCalculating
p 現在計算中です。後ほどご確認ください
else
+rankingTable
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 みす廃ランキング
details
summary これは何?
p みす廃ランキングは、独自に算出された「<strong>みす廃レート</strong>」の高い順ランキングです。毎日みす廃あらーとが発行される度に更新されます。
p みす廃レートは、登録日からの経過日数およびノート数から算出されます。
if state.nowCalculating
p 現在計算中です。後ほどご確認ください
else
+rankingTable

View File

@ -1,15 +1,15 @@
extends _base
block content
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 利用規約
ul
li 本サービスは「現状のまま」「無保証」で提供されます。本サービスを利用したことによる損害などについて、管理人は一切責任を負わないものとします。
li 本サービスは、Misskey プロジェクトとは一切関係がございません。
li ユーザーはインスタンスの諸規約に従った上で本サービスを使うものとします。インスタンスの規約により、自動投稿が禁止されている場合は本サービスを使用しないでください。
li 本サービスでは、接続先のアカウントが存在しない、トークンが失効してしまったなどの場合に、自動的にユーザーアカウントを削除します。
li 本サービスの仕様は、事前の予告無しに変更される可能性があります。
li 本サービスは、事前の予告無しに突然閉鎖される可能性があります。
li 本規約は、事前の予告無しに変更される可能性があります。
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 利用規約
ul
li 本サービスは「現状のまま」「無保証」で提供されます。本サービスを利用したことによる損害などについて、管理人は一切責任を負わないものとします。
li 本サービスは、Misskey プロジェクトとは一切関係がございません。
li ユーザーはインスタンスの諸規約に従った上で本サービスを使うものとします。インスタンスの規約により、自動投稿が禁止されている場合は本サービスを使用しないでください。
li 本サービスでは、接続先のアカウントが存在しない、トークンが失効してしまったなどの場合に、自動的にユーザーアカウントを削除します。
li 本サービスの仕様は、事前の予告無しに変更される可能性があります。
li 本サービスは、事前の予告無しに突然閉鎖される可能性があります。
li 本規約は、事前の予告無しに変更される可能性があります。

View File

@ -1,45 +1,45 @@
extends _base
block content
case from
when 'logout'
.xd-alert.danger: strong ログアウトしました。
when 'optout'
.xd-alert.danger: strong 連携を解除しました。
.xd-card
h1
a(href="/") みす廃あらーと
small: a(href="/about") #{version}
h2= welcomeMessage
section.xd-card
p Misskey は楽しいものです。気がついたら1日中入り浸っていることも多いでしょう。
p さあ、今すぐみす廃アラートをインストールして、今日のあなたの Misskey 活動を把握しよう。
p 始める前に、
a(href="/terms") 利用規約
| を読んでください。
form(action="/login", method="get")
.xd-inputs
input.xd-input(type="text" placeholder="ホスト名(例: misskey.io)" name="host" required)
input.xd-button.primary(type="submit", value="ログイン")
+ranking()
section.xd-hstack
.xd-card
.header
h1.title 開発者
.body
p 何か困ったことがあったら、以下のアカウントにメッセージを送ってください。
ul
li: +exta(href="https://misskey.io/@ebi") @ebi@misskey.io
li: +exta(href="https://groundpolis.app/@X") @X@groundpolis.app
li: +exta(href="https://twitter.com/Xeltica") @Xeltica@twitter.com
li: +exta(href="mailto:xeltica@gmail.com") xeltica@gmail.com
.xd-card
.header
h1.title タイムライン
.body
p 近いうちに、ここで #misshaialert タグのタイムラインを表示します。まだ工事中です
<a href="https://github.com/xeltica/misshaialert" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
case from
when 'logout'
.xd-alert.danger: strong ログアウトしました。
when 'optout'
.xd-alert.danger: strong 連携を解除しました。
.xd-card
h1
a(href="/") みす廃あらーと
small: a(href="/about") #{version}
h2= welcomeMessage
section.xd-card
p Misskey は楽しいものです。気がついたら1日中入り浸っていることも多いでしょう。
p さあ、今すぐみす廃アラートをインストールして、今日のあなたの Misskey 活動を把握しよう。
p 始める前に、
a(href="/terms") 利用規約
| を読んでください。
form(action="/login", method="get")
.xd-inputs
input.xd-input(type="text" placeholder="ホスト名(例: misskey.io)" name="host" required)
input.xd-button.primary(type="submit", value="ログイン")
+ranking()
section.xd-hstack
.xd-card
.header
h1.title 開発者
.body
p 何か困ったことがあったら、以下のアカウントにメッセージを送ってください。
ul
li: +exta(href="https://misskey.io/@ebi") @ebi@misskey.io
li: +exta(href="https://groundpolis.app/@X") @X@groundpolis.app
li: +exta(href="https://twitter.com/Xeltica") @Xeltica@twitter.com
li: +exta(href="mailto:xeltica@gmail.com") xeltica@gmail.com
.xd-card
.header
h1.title タイムライン
.body
p 近いうちに、ここで #misshaialert タグのタイムラインを表示します。まだ工事中です
<a href="https://github.com/xeltica/misshaialert" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
block script
script.
history.replaceState(null, null, '/');
script.
history.replaceState(null, null, '/');

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": {
/* 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'. */
"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. */
"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. */
// "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. */
// "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": [
"node_modules/@types",
"src/@types"
], /* 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'. */
// "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. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"./migration"
]
"compilerOptions": {
"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'. */
"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. */
"strict": true, /* Enable all strict type-checking options. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
"typeRoots": [
"node_modules/@types",
"src/@types"
], /* List of folders to include type definitions from. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"./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