Create website

This commit is contained in:
Dominik Korsa 2021-01-18 19:39:16 +01:00
parent 85c94b3516
commit a4419528a0
No known key found for this signature in database
GPG key ID: 546F986F71A6FE6E
27 changed files with 13652 additions and 0 deletions

View file

@ -3,6 +3,7 @@
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/backend/backend.iml" filepath="$PROJECT_DIR$/backend/backend.iml" /> <module fileurl="file://$PROJECT_DIR$/backend/backend.iml" filepath="$PROJECT_DIR$/backend/backend.iml" />
<module fileurl="file://$PROJECT_DIR$/website/website.iml" filepath="$PROJECT_DIR$/website/website.iml" />
</modules> </modules>
</component> </component>
</project> </project>

View file

@ -19,6 +19,9 @@ export default class Application extends BaseEntity {
@Column() @Column()
public iconUrl!: string | null; public iconUrl!: string | null;
@Column()
public iconColor!: string;
@Column() @Column()
public verified!: boolean; public verified!: boolean;

View file

@ -63,6 +63,7 @@ export default function registerAuthorize(server: MyFastifyInstance): void {
throw new ScopeError(`Unknown scope ${scope}`); throw new ScopeError(`Unknown scope ${scope}`);
} }
}); });
// TODO: Check if user requests student scopes according to students_mode
const promptId = nanoid(12); const promptId = nanoid(12);

View file

@ -10,6 +10,9 @@ export default class PromptInfoApplication {
}) })
public iconUrl!: string | null; public iconUrl!: string | null;
@Field(() => String)
public iconColor!: string;
@Field(() => Boolean) @Field(() => Boolean)
public verified!: boolean; public verified!: boolean;
} }

View file

@ -37,6 +37,7 @@ export default class PromptInfoResolver implements ResolverInterface<PromptInfo>
return { return {
name: application.name, name: application.name,
iconUrl: application.iconUrl, iconUrl: application.iconUrl,
iconColor: application.iconColor,
verified: application.verified, verified: application.verified,
}; };
} }

3
website/.browserslistrc Normal file
View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

7
website/.editorconfig Normal file
View file

@ -0,0 +1,7 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

18
website/.eslintrc.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
};

22
website/.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
website/.npmrc Normal file
View file

@ -0,0 +1 @@
shamefully-hoist=true

24
website/README.md Normal file
View file

@ -0,0 +1,24 @@
# @wulkanowy/bridge-website
## Project setup
```
pnpm install
```
### Compiles and hot-reloads for development
```
pnpm run serve
```
### Compiles and minifies for production
```
pnpm run build
```
### Lints and fixes files
```
pnpm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
website/babel.config.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};

13159
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

36
website/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@wulkanowy/bridge-website",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2",
"vuetify": "^2.2.11"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~3.9.3",
"vue-cli-plugin-vuetify": "~2.0.9",
"vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0"
}
}

BIN
website/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

19
website/public/index.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
</head>
<body>
<noscript>
<strong>This website doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

BIN
website/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

After

Width:  |  Height:  |  Size: 539 B

View file

@ -0,0 +1,228 @@
<template>
<v-app class="authenticate-prompt-app">
<div>
<div class="text-h4 text-center mt-8">
<span class="primary--text">Wulkanowy</span> Bridge
</div>
</div>
<v-main class="px-4">
<v-sheet max-width="500" class="mx-auto mt-16" color="transparent">
<v-alert type="error" text v-if="promptInfoError">
Nie udało się wczytać danych
<template #append>
<v-btn text color="error" @click="loadPromptInfo">
Spróbuj ponownie
</v-btn>
</template>
</v-alert>
<div class="d-flex align-center justify-center mt-16 mb-16" v-else-if="!promptInfo">
<v-progress-circular indeterminate :size="96" color="primary" />
</div>
<template v-else>
<div class="pb-1 text--secondary">
Krok <span class="primary--text">{{ step }}/5</span>
</div>
<v-card outlined>
<div class="d-flex justify-center mn-16 avatar__wrapper">
<v-badge
:color="promptInfo.application.verified ? 'green' : 'grey'"
offset-x="64"
offset-y="16"
bottom
:content="promptInfo.application.verified ? 'Zweryfikowana' : 'Niezweryfikowana'"
:value="step === 1"
>
<transition name="scale">
<v-sheet
v-if="step === 1"
width="128"
height="128"
class="avatar-sheet mx-4 overflow-hidden"
outlined
>
<v-sheet
class="fill-height d-flex align-center justify-center"
:color="promptInfo.application.iconColor"
>
<v-img
:src="promptInfo.application.iconUrl"
width="80"
height="80"
aspect-ratio="1"
contain
>
<template v-slot:placeholder>
<div class="fill-height d-flex align-center justify-center">
<v-icon :size="80">
mdi-help
</v-icon>
</div>
</template>
</v-img>
</v-sheet>
</v-sheet>
</transition>
</v-badge>
</div>
<div class="pt-16">
<h2 class="text-subtitle-1 text--secondary mt-6 mb-2 px-4">
Aplikacja
<span class="text--primary">{{ promptInfo.application.name }}</span>
chce uzyskać dostęp do twojego konta VULCAN UONET+ przez Wulkanowy Bridge
</h2>
<v-subheader>Uprawnienia aplikacji</v-subheader>
<v-list subheader>
<v-list-item v-for="item in scopeItems" :key="item.key">
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ item.title }}
</v-list-item-title>
<v-list-item-subtitle v-if="item.subtitle !== undefined">
{{ item.subtitle }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
<v-alert color="info" text class="mb-2 mx-2">
<span class="font-weight-medium">{{ promptInfo.application.name }}</span>
nie zobaczy twojego hasła
<template #append>
<!-- TODO: Implement -->
<v-btn text color="info">Więcej</v-btn>
</template>
</v-alert>
<v-divider />
</div>
<v-card-actions>
<v-btn color="primary" text outlined>
Odmów
</v-btn>
<v-spacer />
<v-btn color="primary">
Zaloguj się
</v-btn>
</v-card-actions>
</v-card>
</template>
</v-sheet>
</v-main>
</v-app>
</template>
<style lang="scss">
.authenticate-prompt-app {
background-color: #f7f7f7 !important;
.avatar-sheet {
border-radius: 50%;
}
.fill-height {
height: 100%;
}
.avatar__wrapper {
position: absolute;
top: -64px;
width: 100%;
}
.scale-enter-active, .scale-leave-active {
transition: transform 0.3s;
}
.scale-enter, .scale-leave-to {
transform: scale(0);
}
}
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
export enum StudentsMode {
None = 'none',
One = 'one',
Many = 'many',
}
export interface PromptInfo {
scopes: string[];
studentsMode: StudentsMode;
application: {
name: string;
iconUrl: string | null;
iconColor: string;
verified: boolean;
};
}
@Component({
name: 'AuthenticatePromptApp',
})
export default class AuthenticatePromptApp extends Vue {
promptInfo: PromptInfo | null = null;
promptInfoError = false;
step = 1;
readonly scopeDescriptions: Record<string, {
title: string;
subtitle?: string;
icon: string;
}> = {
timetable: {
title: 'Plan lekcji',
icon: 'mdi-timetable',
},
grades: {
title: 'Oceny i punkty',
subtitle: 'Oceny cząstkowe, końcowe, opisowe oraz punkty',
icon: 'mdi-numeric-6-box-multiple-outline',
},
notes: {
title: 'Uwagi',
icon: 'mdi-note-text-outline',
},
achievements: {
title: 'Osiągnięcia',
icon: 'mdi-trophy-outline',
},
}
get scopeItems() {
if (this.promptInfo === null) return undefined;
return this.promptInfo.scopes.map((key: string) => ({
key,
...this.scopeDescriptions[key],
}));
}
async loadPromptInfo() {
this.promptInfoError = false;
this.promptInfo = null;
await new Promise((resolve) => setTimeout(resolve, 1000));
if (Math.random() < 0.5) {
this.promptInfoError = true;
} else {
this.promptInfo = {
scopes: ['grades', 'timetable', 'notes', 'achievements'],
studentsMode: StudentsMode.Many,
application: {
iconColor: '#f00',
iconUrl: null,
name: 'Not a fancy app',
verified: false,
},
};
}
}
async created() {
await this.loadPromptInfo();
}
}
</script>

View file

@ -0,0 +1,10 @@
import Vue from 'vue';
import vuetify from '@/plugins/vuetify';
import AuthenticatePromptApp from './app.vue';
Vue.config.productionTip = false;
new Vue({
vuetify,
render: (h) => h(AuthenticatePromptApp),
}).$mount('#app');

View file

@ -0,0 +1,25 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib/framework';
import pl from 'vuetify/src/locale/pl';
Vue.use(Vuetify);
export default new Vuetify({
theme: {
options: {
customProperties: true,
},
themes: {
light: {
primary: '#d32f2f',
secondary: '#d32f2f',
accent: '#d32f2f',
error: '#ff5722',
},
},
},
lang: {
locales: { pl },
current: 'pl',
},
});

13
website/src/shims-tsx.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

5
website/src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

5
website/src/shims-vuetify.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module 'vuetify/lib/framework' {
import Vuetify from 'vuetify';
export default Vuetify;
}

40
website/tsconfig.json Normal file
View file

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

13
website/vue.config.js Normal file
View file

@ -0,0 +1,13 @@
module.exports = {
transpileDependencies: [
'vuetify',
],
pages: {
'authenticate-prompt': {
entry: 'src/pages/authenticate-prompt/main.ts',
template: 'public/index.html',
filename: 'authenticate-prompt.html',
title: 'Authorize application | Wulkanowy Bridge',
},
},
};

9
website/website.iml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>