Commit 28787594 authored by nanahira's avatar nanahira

first

parent c5d6e9b1
/src/app/api
dist/*
build/*
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.*.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
stages:
- install
- build
- deploy
variables:
GIT_DEPTH: "1"
npm_ci:
stage: install
tags:
- linux
script:
- npm ci
artifacts:
paths:
- node_modules
.build_base:
stage: build
tags:
- linux
dependencies:
- npm_ci
build:
extends: .build_base
script: npm run build
artifacts:
paths:
- dist/
test:
extends: .build_base
script: npm run test
#upload_to_minio:
# stage: deploy
# dependencies:
# - build
# tags:
# - linux
# script:
# - aws s3 --endpoint=https://minio.mycard.moe:9000 sync dist/my-project/ s3://nanahira/my-project
# only:
# - master
#!/bin/bash
npm install --save \
bootstrap \
@ng-bootstrap/ng-bootstrap
npm install --save-dev \
'@typescript-eslint/eslint-plugin@^4.28.2' \
'@typescript-eslint/parser@^4.28.2 '\
'eslint@^7.30.0' \
'eslint-config-prettier@^8.3.0' \
'eslint-plugin-prettier@^3.4.0'
This source diff could not be displayed because it is too large. You can view the blob instead.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Route, RouterModule, Routes } from '@angular/router';
import { TournamentComponent } from './tournament/tournament.component';
const routes: Routes = [];
export interface Menu extends Route {
title: string;
}
export const Menus: Menu[] = [
{ path: 'tournament', component: TournamentComponent, title: '比赛' },
];
const routes: Routes = [
...Menus,
{ path: '', redirectTo: 'tournament', pathMatch: 'full' },
{ path: '**', redirectTo: 'tournament', pathMatch: 'full' },
{
path: '**',
redirectTo: 'tournament',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}
:host {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.bottom {
flex: 1;
display: flex;
}
.right {
flex: 1;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
main {
display: flex;
flex-wrap: nowrap;
height: 100vh;
height: -webkit-fill-available;
max-height: 100vh;
overflow-x: auto;
overflow-y: hidden;
}
.b-example-divider {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.bi {
vertical-align: -.125em;
pointer-events: none;
fill: currentColor;
}
.dropdown-toggle {
outline: 0;
}
.nav-flush .nav-link {
border-radius: 0;
}
.btn-toggle {
display: inline-flex;
align-items: center;
padding: .25rem .5rem;
font-weight: 600;
color: rgba(0, 0, 0, .65);
background-color: transparent;
border: 0;
}
.btn-toggle:hover,
.btn-toggle:focus {
color: rgba(0, 0, 0, .85);
background-color: #d2f4ea;
}
.btn-toggle::before {
width: 1.25em;
line-height: 0;
content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
transition: transform .35s ease;
transform-origin: .5em 50%;
}
.btn-toggle[aria-expanded="true"] {
color: rgba(0, 0, 0, .85);
}
.btn-toggle[aria-expanded="true"]::before {
transform: rotate(90deg);
}
.btn-toggle-nav a {
display: inline-flex;
padding: .1875rem .5rem;
margin-top: .125rem;
margin-left: 1.25rem;
text-decoration: none;
}
.btn-toggle-nav a:hover,
.btn-toggle-nav a:focus {
background-color: #d2f4ea;
}
.scrollarea {
overflow-y: auto;
}
.fw-semibold {
font-weight: 600;
}
.lh-tight {
line-height: 1.25;
}
This diff is collapsed.
import { Component } from '@angular/core';
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { UserInfoService } from './user-info.service';
import { Menus } from './app-routing.module';
import { Tooltip } from 'bootstrap';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'tabulator-web';
export class AppComponent implements AfterViewInit {
title = 'MyCard 赛事排表器';
menus = Menus;
constructor(
public userInfo: UserInfoService,
private current: ActivatedRoute
) {}
ngAfterViewInit() {
const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]')
);
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new Tooltip(tooltipTriggerEl);
});
}
}
......@@ -3,16 +3,14 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ToastComponent } from './toast/toast.component';
import { TournamentComponent } from './tournament/tournament.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
declarations: [AppComponent, ToastComponent, TournamentComponent],
imports: [BrowserModule, AppRoutingModule, NgbModule],
providers: [],
bootstrap: [AppComponent]
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}
import { TestBed } from '@angular/core/testing';
import { AuthGuard } from './auth.guard';
describe('AuthGuard', () => {
let guard: AuthGuard;
beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(AuthGuard);
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
});
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
RouterStateSnapshot,
} from '@angular/router';
import { UserInfoService } from './user-info.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private userInfo: UserInfoService) {}
async check() {
return !!this.userInfo.user;
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.check();
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
return this.check();
}
}
import { TestBed } from '@angular/core/testing';
import { ToastService } from './toast.service';
describe('ToastService', () => {
let service: ToastService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ToastService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
import { Injectable } from '@angular/core';
export interface ToastInfo {
id: number;
header: string;
body: string;
persist?: boolean;
classname?: string;
}
@Injectable({
providedIn: 'root',
})
export class ToastService {
currentId = 0;
toasts: ToastInfo[] = [];
show(header: string, body: string, classname?: string) {
const id = ++this.currentId;
this.toasts.push({ id, header, body, classname });
return id;
}
hide(id: number) {
const index = this.toasts.findIndex((t) => t.id === id);
if (index !== -1) {
this.toasts.splice(index, 1);
}
}
error(message: string) {
this.show('错误', message, 'bg-danger text-light');
}
warn(message: string) {
this.show('警告', message, 'bg-warning');
}
success(message?: string) {
this.show('成功', message || '操作成功', 'bg-success text-light');
}
attention(message: string) {
this.show('注意', message, 'bg-primary');
}
info(message: string) {
this.show('消息', message);
}
constructor() {}
}
<ngb-toast
*ngFor="let toast of toastService.toasts"
[class]="toast.classname"
[header]="toast.header" [autohide]="!toast.persist" [delay]="5000"
(hidden)="toastService.hide(toast.id)"
class="ngb-toasts"
>{{toast.body}}</ngb-toast>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ToastComponent } from './toast.component';
describe('ToastComponent', () => {
let component: ToastComponent;
let fixture: ComponentFixture<ToastComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ToastComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ToastComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
import { ToastService } from '../toast.service';
@Component({
selector: 'app-toasts',
templateUrl: './toast.component.html',
styleUrls: ['./toast.component.css'],
})
export class ToastComponent implements OnInit {
constructor(public toastService: ToastService) {}
ngOnInit(): void {}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TournamentComponent } from './tournament.component';
describe('TournamentComponent', () => {
let component: TournamentComponent;
let fixture: ComponentFixture<TournamentComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ TournamentComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TournamentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-tournament',
templateUrl: './tournament.component.html',
styleUrls: ['./tournament.component.css']
})
export class TournamentComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
import { TestBed } from '@angular/core/testing';
import { UserInfoService } from './user-info.service';
describe('UserInfoService', () => {
let service: UserInfoService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserInfoService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
import { Injectable } from '@angular/core';
import { MyCardSSOUser } from './utility/MyCardSSOUser';
import { ActivatedRoute } from '@angular/router';
import { loginUrl } from './utility/login-url';
import { Buffer } from 'buffer';
@Injectable({
providedIn: 'root',
})
export class UserInfoService {
user?: MyCardSSOUser;
constructor() {
this.initUser();
}
initUser() {
const ssoString = new URLSearchParams(window.location.search).get('sso');
const loginString = ssoString || localStorage.getItem('login');
if (!loginString) {
return;
}
localStorage.setItem('login', loginString);
const decodedLoginString = Buffer.from(loginString, 'base64').toString();
const params = new URLSearchParams(decodedLoginString);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.user = Object.fromEntries(params);
}
getHeader(): { Authorization?: string } {
if (!this.user) {
return {};
}
return { Authorization: `Bearer ${this.user.token}` };
}
login() {
const currentUrl = window.location.href;
window.location.replace(loginUrl(currentUrl));
}
logout() {
this.user = undefined;
localStorage.removeItem('login');
return this.login();
}
}
export interface MyCardSSOUser {
id: string;
username: string;
name: string;
email: string;
password_hash: string;
salt: string;
active: string;
admin: string;
avatar: string;
locale: string;
registration_ip_address: string;
ip_address: string;
created_at: string;
updated_at: string;
return_sso_url: string;
external_id: string;
avatar_url: string;
avatar_force_update: string;
token: string;
}
import { Buffer } from 'buffer';
export function loginUrl(callbackUrl: string) {
let params = new URLSearchParams();
params.set('return_sso_url', callbackUrl);
const payload = Buffer.from(params.toString()).toString('base64');
const url = new URL('https://accounts.moecube.com/signin');
params = url['searchParams'];
params.set('sso', payload);
return url.toString();
}
src/favicon.ico

948 Bytes | W: | H:

src/favicon.ico

24.4 KB | W: | H:

src/favicon.ico
src/favicon.ico
src/favicon.ico
src/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
......@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>TabulatorWeb</title>
<title>MyCard 赛事排表器</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
......
/* You can add global styles to this file, and also import other style files */
@import '~bootstrap';
body {
min-height: 100vh;
min-height: -webkit-fill-available;
}
html {
height: -webkit-fill-available;
}
/*干掉站长统计的文本*/
a[title="站长统计"] {
display: none;
}
/*标题*/
h1.title {
font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
font-size: 22px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.index .navbar-nav>li.index>a, .index .navbar-nav>li.index>a:focus, .index .navbar-nav>li.index>a:hover,
.download .navbar-nav>li.download>a, .download .navbar-nav>li.download>a:focus, .download .navbar-nav>li.download>a:hover,
.usage .navbar-nav>li.usage>a, .usage .navbar-nav>li.usage>a:focus, .usage .navbar-nav>li.usage>a:hover,
.changelog .navbar-nav>li.changelog>a, .changelog .navbar-nav>li.changelog>a:focus, .changelog .navbar-nav>li.changelog>a:hover,
.bugs .navbar-nav>li.bugs>a, .bugs .navbar-nav>li.bugs>a:focus, .bugs .navbar-nav>li.bugs>a:hover,
.lab .navbar-nav>li.lab>a, .lab .navbar-nav>li.lab>a:focus, .lab .navbar-nav>li.lab>a:hover,
.pre .navbar-nav>li.pre>a, .pre .navbar-nav>li.pre>a:focus, .pre .navbar-nav>li.pre>a:hover
{
color: #555;
background-color: #e7e7e7;
}
h1.title .date {
font-size: 14px;
float: right;
padding-top: 10px;
}
h2.title {
font-family: 'Helvetica Neue', Helvetica, 'Microsoft Yahei', 'Hiragino Sans GB', 'WenQuanYi Micro Hei', sans-serif;
font-size: 16px;
padding: 8px 0 8px 8px;
border-left: 2px solid #ddd;
}
footer {
/*margin: 100px 0 10px 0;*/
color: #767676;
text-align: center;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment