wip
This commit is contained in:
parent
66ced29c6d
commit
dbcfdcd0c3
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
|
@ -18,7 +18,7 @@ module.exports = {
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
'tab'
|
||||
'tab',
|
||||
],
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"ext": "ts,tsx,pug,scss",
|
||||
"exec": "run-s build start"
|
||||
}
|
161
package.json
161
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { MetaController } from "./meta";
|
||||
import { RankingController } from "./ranking";
|
||||
|
||||
export default [
|
||||
MetaController,
|
||||
RankingController,
|
||||
];
|
@ -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: 'ほに',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
14
src/controllers/session.ts
Normal file
14
src/controllers/session.ts
Normal 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
6
src/die.ts
Normal 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
33
src/frontend/App.tsx
Normal 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>
|
||||
);
|
15
src/frontend/components/DeveloperInfo.tsx
Normal file
15
src/frontend/components/DeveloperInfo.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
15
src/frontend/components/HashtagTimeline.tsx
Normal file
15
src/frontend/components/HashtagTimeline.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
24
src/frontend/components/Header.tsx
Normal file
24
src/frontend/components/Header.tsx
Normal 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>
|
||||
);
|
||||
};
|
31
src/frontend/components/LoginForm.tsx
Normal file
31
src/frontend/components/LoginForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
75
src/frontend/components/Ranking.tsx
Normal file
75
src/frontend/components/Ranking.tsx
Normal 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
5
src/frontend/init.tsx
Normal 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'));
|
45
src/frontend/pages/index.tsx
Normal file
45
src/frontend/pages/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
19
src/frontend/pages/ranking.tsx
Normal file
19
src/frontend/pages/ranking.tsx
Normal 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
9
src/frontend/style.scss
Normal 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;
|
||||
}
|
17
src/frontend/tsconfig.json
Normal file
17
src/frontend/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
@ -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
10
src/render.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import views from 'koa-views';
|
||||
|
||||
|
||||
import constant from './const';
|
||||
|
||||
export const render = views(__dirname + '/views', {
|
||||
extension: 'pug', options: {
|
||||
...constant,
|
||||
}
|
||||
});
|
@ -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('/');
|
||||
}
|
@ -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 = [ '人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...' ];
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
31
src/views/frontend.pug
Normal 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`)
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 本規約は、事前の予告無しに変更される可能性があります。
|
||||
|
@ -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
300
styles/_colors.scss
Normal 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
501
styles/_xeltica-design.scss
Normal 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
58
styles/style.scss
Normal 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;
|
||||
}
|
@ -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
92
webpack.config.js
Normal 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'
|
||||
};
|
Loading…
Reference in New Issue
Block a user