This commit is contained in:
Crow Crowcrow 2018-12-03 20:27:07 +01:00
commit c1d231c201
Signed by: Crow
GPG Key ID: 45A8E203AF859FD8
25 changed files with 18708 additions and 0 deletions

3
.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 8

21
.gitignore vendored Normal file
View File

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

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# meikan
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your tests
```
npm run test
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
}

10590
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "meikan",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@types/lodash": "^4.14.119",
"lodash": "^4.17.11",
"material-design-icons-iconfont": "^4.0.3",
"vue": "^2.5.17",
"vue-class-component": "^6.0.0",
"vue-material": "^1.0.0-beta-10.2",
"vue-property-decorator": "^7.0.0",
"vue-router": "3.0.1",
"vuex": "^3.0.1",
"vuex-persist": "^2.0.0",
"vuex-typex": "^3.0.1",
"vuex-xhr-state": "^0.2.7"
},
"devDependencies": {
"@types/vue-material": "git+https://github.com/calebsander/vue-material-types.git",
"@vue/cli-plugin-babel": "^3.2.0",
"@vue/cli-plugin-typescript": "^3.2.0",
"@vue/cli-service": "^3.2.0",
"lint-staged": "^7.2.2",
"typescript": "^3.0.0",
"vue-template-compiler": "^2.5.17"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.ts": [
"vue-cli-service lint",
"git add"
],
"*.vue": [
"vue-cli-service lint",
"git add"
]
}
}

5
postcss.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

20
public/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<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="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,400italic|Material+Icons">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>meikan</title>
</head>
<body>
<noscript>
<strong>We're sorry but meikan 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>

5
src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div id="app">
<router-view/>
</div>
</template>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

20
src/domain/index.ts Normal file
View File

@ -0,0 +1,20 @@
export const GetAnimeList = async (userID: number): Promise<UserAnime[]> => {
const res = await fetch(`https://api.meikan.moe/v1/users/${userID}/anime`)
const anime: UserAnime[] = await res.json()
return anime
}
export const FindUsers = async (name: string): Promise<User[]> => {
const res = await fetch('https://api.meikan.moe/v1/users', {
method: 'POST',
mode: 'cors',
body: JSON.stringify({
name,
}),
})
const users: User[] = await res.json()
return users
}

29
src/layouts/Base.vue Normal file
View File

@ -0,0 +1,29 @@
<template>
<div>
<md-toolbar class="md-primary" md-elevation="0">
<h3 class="md-title">Meikan</h3>
<div class="md-toolbar-section-end">
<h3 v-if="currentUser" class="md-subheading">
{{ currentUser.name }}
</h3>
</div>
</md-toolbar>
<md-tabs class="md-primary" md-sync-route>
<md-tab id="tab-user" md-label="Users" to="/"></md-tab>
<md-tab v-if="currentUser" id="tab-anime" md-label="Anime" to="/anime"></md-tab>
</md-tabs>
<router-view/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { namespace } from 'vuex-class'
const user = namespace('user')
@Component({})
export default class Base extends Vue {
@user.Getter('currentUser') private currentUser!: User | null
}
</script>

18
src/main.ts Normal file
View File

@ -0,0 +1,18 @@
import Vue from 'vue'
import VueMaterial from 'vue-material'
import 'vue-material/dist/theme/default-dark.css'
import 'vue-material/dist/vue-material.min.css'
Vue.use(VueMaterial)
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app')

32
src/router.ts Normal file
View File

@ -0,0 +1,32 @@
import Store from '@/store'
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Base from './layouts/Base.vue'
import UserAnime from './views/UserAnime.vue'
import UserSelect from './views/UserSelect.vue'
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
component: Base,
children: [
{
path: '',
name: 'userSelect',
component: UserSelect,
},
{
path: 'anime',
name: 'userAnime',
component: UserAnime,
},
],
},
],
})

27
src/store/index.ts Normal file
View File

@ -0,0 +1,27 @@
import Vue from 'vue'
import Vuex from 'vuex'
import VuexPersistence from 'vuex-persist'
import { getStoreBuilder } from 'vuex-typex'
import './module/user'
import { UserState } from './module/user'
import './module/userAnime'
import { UserAnimeState } from './module/userAnime'
Vue.use(Vuex)
export interface RootState {
user: UserState
userAnime: UserAnimeState
}
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
})
const store = getStoreBuilder<RootState>().vuexStore({
plugins: [vuexLocal.plugin],
})
export default store

57
src/store/module/user.ts Normal file
View File

@ -0,0 +1,57 @@
import { FindUsers } from '@/domain'
import { RootState } from '@/store'
import { BareActionContext, getStoreBuilder } from 'vuex-typex'
export interface UserState {
currentUser: User | null
foundUsers: User[]
}
const initialState: UserState = {
currentUser: null,
foundUsers: [],
}
const builder = getStoreBuilder<RootState>().module('user', initialState)
// Getters
const getterUserList = builder.read(function foundUsers(state: UserState) {
return state.foundUsers
})
const getterCurrentUser = builder.read(function currentUser(state: UserState) {
return state.currentUser
})
// Mutations
function setUserList(state: UserState, foundUsers: User[]) {
state.foundUsers = foundUsers
}
function setCurrentUser(state: UserState, currentUser: User) {
state.currentUser = currentUser
}
// Action
async function findUsers(context: BareActionContext<UserState, RootState>, name: string) {
const list = await FindUsers(name)
user.setUserList(list || [])
}
function resetFoundUsers(context: BareActionContext<UserState, RootState>) {
user.setUserList([])
}
export const user = {
get foundUsers(): User[] {
return getterUserList()
},
getCurrentUser(): User | null {
return getterCurrentUser()
},
setCurrentUser: builder.commit(setCurrentUser),
setUserList: builder.commit(setUserList),
resetFoundUsers: builder.dispatch(resetFoundUsers),
findUsers: builder.dispatch(findUsers),
}

View File

@ -0,0 +1,43 @@
import { GetAnimeList } from '@/domain'
import { RootState } from '@/store'
import { BareActionContext, getStoreBuilder } from 'vuex-typex'
export interface UserAnimeState {
anime: UserAnime[]
}
const initialState: UserAnimeState = {
anime: [],
}
const builder = getStoreBuilder<RootState>().module('userAnime', initialState)
// Getters
const getterUserAnime = builder.read(function anime(state: UserAnimeState) {
return state.anime
})
// Mutations
function setAnime(state: UserAnimeState, animeList: UserAnime[]) {
state.anime = animeList
}
// Action
async function fetchUserAnime(context: BareActionContext<UserAnimeState, RootState>, user: User) {
const list = await GetAnimeList(user.id)
userAnime.setAnime(list)
}
function resetUserAnime(context: BareActionContext<UserAnimeState, RootState>) {
userAnime.setAnime([])
}
export const userAnime = {
get anime(): UserAnime[] {
return getterUserAnime()
},
setAnime: builder.commit(setAnime),
fetchUserAnime: builder.dispatch(fetchUserAnime),
resetUserAnime: builder.dispatch(resetUserAnime),
}

14
src/types/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
interface User {
id: number
name: string
}
interface UserAnime {
id: number
title: string
}

54
src/views/UserAnime.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<md-table v-model="anime">
<md-table-toolbar>
<h1 class="md-title md-toolbar-section-start">{{ currentUser.name }}'s anime</h1>
<md-button
class="md-icon-button md-primary"
@click="refresh"
>
<md-icon>refresh</md-icon>
</md-button>
</md-table-toolbar>
<md-table-empty-state>
<md-progress-spinner class="md-accent" md-mode="indeterminate"></md-progress-spinner>
</md-table-empty-state>
<md-table-row slot="md-table-row" slot-scope="{ item }">
<md-table-cell md-label="#" md-numeric width="100">{{ item.anime.id }}</md-table-cell>
<md-table-cell md-label="Title" md-sort-by="title">{{ item.anime.title }}</md-table-cell>
</md-table-row>
</md-table>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { namespace } from 'vuex-class'
const user = namespace('user')
const userAnime = namespace('userAnime')
@Component({})
export default class Home extends Vue {
@user.Getter
private currentUser!: User
@userAnime.Action
private resetUserAnime!: () => void
@userAnime.Action
private fetchUserAnime!: (user: User) => Promise<UserAnime[]>
@userAnime.Getter
private anime!: UserAnime[]
private created() {
if (this.anime.length === 0) {
this.fetchUserAnime(this.currentUser)
}
}
private refresh() {
this.resetUserAnime()
this.fetchUserAnime(this.currentUser)
}
}
</script>

63
src/views/UserSelect.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<div>
<md-table v-model="foundUsers" @md-selected="onSelect">
<md-table-toolbar>
<div class="md-toolbar-section-start">
<h1 class="md-title">Users</h1>
</div>
<md-field md-clearable class="md-toolbar-section-end">
<md-input placeholder="Search users" @input="onInput" />
</md-field>
</md-table-toolbar>
<md-table-row slot="md-table-row" slot-scope="{ item }" md-selectable="single">
<md-table-cell md-label="#" md-numeric width="100">{{ item.id }}</md-table-cell>
<md-table-cell md-label="Title" md-sort-by="title">{{ item.name }}</md-table-cell>
</md-table-row>
</md-table>
</div>
</template>
<script lang="ts">
import _ from 'lodash'
import { Component, Vue } from 'vue-property-decorator'
import { namespace } from 'vuex-class'
const userStore = namespace('user')
const userAnimeStore = namespace('userAnime')
@Component({})
export default class Home extends Vue {
@userStore.Mutation('setCurrentUser')
private setCurrentUser!: (user: User) => void
@userStore.Action('findUsers')
private findUsers!: (name: string) => Promise<User[]>
@userStore.Action('resetFoundUsers')
private resetFoundUsers!: () => void
@userStore.Getter('foundUsers')
private foundUsers!: User[]
@userAnimeStore.Action
private resetUserAnime!: () => void
private onInput = _.debounce((name: string) => {
if (name === '') {
this.resetFoundUsers()
return
}
this.findUsers(name)
}, 200)
private onSelect(user: User) {
this.setCurrentUser(user)
this.resetUserAnime()
this.$router.push({ name: 'userAnime' })
}
}
</script>

40
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,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"noImplicitAny": 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",
]
}

20
tslint.json Normal file
View File

@ -0,0 +1,20 @@
{
"defaultSeverity": "warning",
"extends": [
"tslint:recommended"
],
"linterOptions": {
"exclude": [
"node_modules/**"
]
},
"rules": {
"quotemark": [true, "single"],
"indent": [true, "tabs", 4],
"interface-name": false,
"ordered-imports": true,
"object-literal-sort-keys": false,
"no-consecutive-blank-lines": true,
"semicolon": [true, "never"]
}
}

3
vue.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
lintOnSave: false
}

7564
yarn.lock Normal file

File diff suppressed because it is too large Load Diff