Crow Crowcrow 1 year ago
commit
c1d231c201
Signed by: Crow <robin@ultraware.nl> GPG Key ID: 45A8E203AF859FD8
25 changed files with 18708 additions and 0 deletions
  1. +3
    -0
      .browserslistrc
  2. +21
    -0
      .gitignore
  3. +29
    -0
      README.md
  4. +5
    -0
      babel.config.js
  5. +10590
    -0
      package-lock.json
  6. +46
    -0
      package.json
  7. +5
    -0
      postcss.config.js
  8. BIN
      public/favicon.ico
  9. +20
    -0
      public/index.html
  10. +5
    -0
      src/App.vue
  11. BIN
      src/assets/logo.png
  12. +20
    -0
      src/domain/index.ts
  13. +29
    -0
      src/layouts/Base.vue
  14. +18
    -0
      src/main.ts
  15. +32
    -0
      src/router.ts
  16. +27
    -0
      src/store/index.ts
  17. +57
    -0
      src/store/module/user.ts
  18. +43
    -0
      src/store/module/userAnime.ts
  19. +14
    -0
      src/types/shims-vue.d.ts
  20. +54
    -0
      src/views/UserAnime.vue
  21. +63
    -0
      src/views/UserSelect.vue
  22. +40
    -0
      tsconfig.json
  23. +20
    -0
      tslint.json
  24. +3
    -0
      vue.config.js
  25. +7564
    -0
      yarn.lock

+ 3
- 0
.browserslistrc View File

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

+ 21
- 0
.gitignore 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
- 0
README.md 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
- 0
babel.config.js View File

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

+ 10590
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 46
- 0
package.json 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
- 0
postcss.config.js View File

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

BIN
public/favicon.ico View File

Before After

+ 20
- 0
public/index.html 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
- 0
src/App.vue View File

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

BIN
src/assets/logo.png View File

Before After
Width: 200  |  Height: 200  |  Size: 6.7 KiB

+ 20
- 0
src/domain/index.ts 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
- 0
src/layouts/Base.vue 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
- 0
src/main.ts 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
- 0
src/router.ts 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
- 0
src/store/index.ts 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
- 0
src/store/module/user.ts 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),
}

+ 43
- 0
src/store/module/userAnime.ts 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
- 0
src/types/shims-vue.d.ts 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
- 0
src/views/UserAnime.vue 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
- 0
src/views/UserSelect.vue 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
- 0
tsconfig.json 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
- 0
tslint.json 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
- 0
vue.config.js View File

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

+ 7564
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save