Compare commits

...

No commits in common. "master" and "ts" have entirely different histories.
master ... ts

43 changed files with 18773 additions and 1318 deletions

3
.browserslistrc Normal file
View File

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

27
.gitignore vendored
View File

@ -1,8 +1,21 @@
.DS_Store
node_modules
gulpfile.js
sftp-config.json
build
meikan
static/css
compile.bat
meikan.sublime-workspace
/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*

View File

@ -1 +1,29 @@
A [Meikan](https://git.fuyu.moe/Fuyu/meikan) client with a twist. Lets hope it will be beautiful.
# 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/).

View File

@ -1,72 +0,0 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"path"
)
type Genre struct {
Name string `json:"name"`
Level int `json:"level"`
}
type Anime struct {
ID int `json:"id"`
Title string `json:"title"`
Type string `json:"type"`
Episodes int `json:"episodes"`
State string `json:"state"`
Rating string `json:"rating"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Genres []Genre `json:"genres"`
AverageDuration int `json:"average_duration"`
AnidbID int `json:"anidb_id"`
MyanimelistID int `json:"myanimelist_id"`
}
type AnimeSearch struct {
Title string `json:"title"`
}
var baseURL = "https://api.meikan.moe/v1/"
func getJSON(url string, target interface{}) error {
r, err := http.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(&target)
}
func postJSON(url string, form AnimeSearch, target interface{}) error {
b := new(bytes.Buffer)
json.NewEncoder(b).Encode(form)
r, err := http.Post(url, "application/json; charset=utf-8", b)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
}
func AnimeByID(id string) (Anime, error) {
anime := Anime{}
url := baseURL + path.Join("anime", id)
err := getJSON(url, &anime)
return anime, err
}
func SearchAnime(title string) ([]Anime, error) {
anime := []Anime{}
form := AnimeSearch{Title: title}
url := baseURL + "anime"
err := postJSON(url, form, &anime)
return anime, err
}

5
babel.config.js Normal file
View File

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

View File

@ -1,5 +0,0 @@
{{ define "footer" }}
<script src="/static/js/menu.js"></script>
</body>
</html>
{{ end }}

View File

@ -1,260 +0,0 @@
{{ define "header" }}
<!DOCTYPE html>
<html>
<head>
<title>Meikan - Home</title>
<link rel="stylesheet" type="text/css" href="/static/css/base.css">
</head>
<body>
<div class="menu">
<div class="container">
<a class="logo" href="/">
<i class="material-icons">local_library</i><span>Meikan</span>
</a>
<a data-group="anime" href="/anime">
anime
</a>
<a data-group="manga" href="/manga">
manga
</a>
<a data-group="vn" href="/vn">
vn
</a>
</div>
</div>
<div id="subMenu" class="submenu">
<div class="container">
<div data-group="anime" class="group">
<div class="column">
<div class="groupTitle">Top Anime</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Top Genre</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Random</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
</div>
<div data-group="manga" class="group">
<div class="column">
<div class="groupTitle">Top Manga</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Top Genre</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Random</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
</div>
<div data-group="vn" class="group">
<div class="column">
<div class="groupTitle">Top VN</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Top Genre</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
<div class="column">
<div class="groupTitle">Random</div>
<ul>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
<li>
<div class="title">Something</div>
<div class="info">TV | 10 eps | Finished</div>
</li>
</ul>
</div>
</div>
</div>
</div>
{{ end }}

79
main.go
View File

@ -1,79 +0,0 @@
package main
import (
"fmt"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"./api"
"github.com/julienschmidt/httprouter"
)
const sockPath = "/srv/kumori.moe/sock/meikan.sock"
var tmpl = template.Must(template.ParseGlob("templates/*.gohtml"))
func init() {
l, _ := ioutil.ReadDir("components")
for _, v := range l {
c, _ := ioutil.ReadFile(`components/` + v.Name())
tmpl, _ = tmpl.Parse(string(c))
}
}
func main() {
os.Remove(sockPath)
sock, err := net.Listen("unix", sockPath)
if err != nil {
log.Fatalln(err)
return
}
router := httprouter.New()
router.GET("/", getIndex)
router.GET("/anime", getAnime)
router.GET("/reload", reload)
router.ServeFiles("/static/*filepath", http.Dir("static/"))
err = os.Chmod(sockPath, 0770)
if err != nil {
log.Fatalln(err)
}
log.Fatal(http.Serve(sock, router))
}
func getIndex(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
err := tmpl.ExecuteTemplate(w, "home.gohtml", nil)
if err != nil {
println(err)
}
}
func getAnime(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
animeList, err := api.SearchAnime("tsukaima")
if err != nil {
fmt.Fprint(w, err)
return
}
err = tmpl.ExecuteTemplate(w, "anime.gohtml", &animeList)
if err != nil {
println(err)
}
}
func reload(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
tmpl = template.Must(template.ParseGlob("templates/*.gohtml"))
l, _ := ioutil.ReadDir("components")
for _, v := range l {
c, _ := ioutil.ReadFile(`components/` + v.Name())
tmpl, _ = tmpl.Parse(string(c))
}
}

10590
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +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": {
"gulp": "^3.9.1",
"gulp-sass": "^3.1.0",
"gulp-sftp-with-callbacks": "^0.1.8",
"request": "^2.81.0",
"scp2": "^0.5.0"
"@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>

View File

@ -1,74 +0,0 @@
@import 'variables';
.search {
display: flex;
.filters {
width: 300px;
}
.cardList {
padding: 20px 0;
flex: 1;
.card {
display: flex;
overflow: hidden;
border-radius: 2px;
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24);
&:not(:last-child) {
margin-bottom: 20px;
}
img {
height: 200px;
}
.body {
display: flex;
flex-direction: column;
max-height: 200px;
flex: 1;
.info {
display: flex;
overflow: hidden;
flex-direction: column;
padding: 16px 16px 0;
flex: 1;
.title {
font-size: 25px;
color: $pink;
}
.synopsis {
overflow: hidden;
display: -webkit-box;
padding: 16px 0 0 0;
color: rgba(0,0,0,.52);
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
margin-bottom: 16px;
}
}
.action {
padding: 16px;
border-top: 1px rgba(0,0,0,.12) solid;
button {
font-size: 16px;
color: #448aff;
border: none;
background: none;
outline: none;
}
}
}
}
}
}

View File

@ -1,217 +0,0 @@
@font-face {
font-family: 'Roboto';
font-weight: 400;
font-style: normal;
src: local('Roboto'), local('Roboto-Regular'), url(/static/fonts/roboto.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
}
@font-face {
font-family: 'Material Icons';
font-weight: 400;
font-style: normal;
src: local('Material Icons'), local('MaterialIcons-Regular'), url(/static/fonts/material-icons.woff2) format('woff2');
}
@import 'variables';
* {
box-sizing: border-box;
}
html,
body {
font-family: 'Roboto', sans-serif;
height: 100%;
margin: 0;
}
body {
&:after {
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
content: '';
opacity: .05;
background: url('/static/img/bg.svg');
background-size: cover;
}
.material-icons {
font-family: 'Material Icons';
font-size: 24px;
font-weight: normal;
font-style: normal;
line-height: 1;
display: inline-block;
white-space: nowrap;
letter-spacing: normal;
text-transform: none;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
.container {
position: relative;
width: 1240px;
margin: 0 auto;
}
.menu {
font-size: 0;
position: relative;
z-index: 2;
height: 65px;
margin: 0;
padding: 0;
background: #fff;
box-shadow: 0 3px 6px rgba(0,0,0,.23), 0 3px 6px rgba(0,0,0,.16);
a {
font-size: 22px;
font-variant: small-caps;
line-height: 65px;
display: inline-block;
height: 100%;
padding: 0 30px;
transition: background .1s, color .1s;
vertical-align: middle;
text-decoration: none;
color: rgba(0,0,0,.52);
&:first-child {
font-size: 30px;
font-variant: normal;
padding-left: 0;
color: $pink;
> i {
font-size: 30px;
line-height: inherit;
height: inherit;
vertical-align: top;
}
> span {
margin-left: 10px;
}
}
&:not(:first-child):hover,
&:not(:first-child).open {
color: #fff;
background: $pink;
}
}
}
.submenu {
position: absolute;
z-index: 1;
top: 65px;
overflow: hidden;
width: 100%;
height: 0;
transition: height .5s;
background: #212121;
box-shadow: inset 0 3px 6px rgba(0,0,0,.23);
will-change: height;
&.open {
height: 350px;
}
.container {
height: 350px;
.group {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0;
&.open {
opacity: 1;
}
.column {
display: flex;
flex-direction: column;
margin: 20px 10px;
color: #fff;
flex: 1;
.groupTitle {
line-height: 20px;
max-height: 20px;
}
> * {
display: flex;
height: 100%;
}
ul {
display: flex;
flex-direction: column;
margin: 20px 0 0 10px;
padding: 10px 20px;
list-style: none;
border-left: 2px rgba(255,255,255,.1) solid;
li {
position: relative;
flex: 1;
.title {
font-size: 16px;
display: block;
margin-bottom: 3px;
color: #ff80ab;
}
.info {
font-size: .8em;
font-weight: light;
color: #aaa;
}
}
}
}
}
}
}
}

View File

@ -1,159 +0,0 @@
.menu
{
font-size: 0;
position: relative;
z-index: 2;
height: 65px;
margin: 0;
padding: 0;
background: #fff;
box-shadow: 0 3px 6px rgba(0,0,0,.23), 0 3px 6px rgba(0,0,0,.16);
a
{
font-size: 22px;
font-variant: small-caps;
line-height: 65px;
display: inline-block;
height: 100%;
padding: 0 30px;
transition: background .1s, color .1s;
vertical-align: middle;
text-decoration: none;
color: rgba(0,0,0,.52);
&:first-child
{
font-size: 30px;
font-variant: normal;
padding-left: 0;
color: #ff4081;
> i {
height: inherit;
line-height: inherit;
vertical-align: top;
font-size: 30px;
}
> span
{
margin-left: 10px;
}
}
&:not(:first-child):hover,
&:not(:first-child).open
{
color: #fff;
background: #ff4081;
}
}
}
.submenu
{
position: absolute;
top: 65px;
z-index: 1;
overflow: hidden;
width: 100%;
height: 0;
transition: height .5s;
background: #212121;
box-shadow: inset 0 3px 6px rgba(0,0,0,.23);
will-change: height;
&.open
{
height: 350px;
}
.container
{
height: 350px;
.group
{
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0;
&.open
{
opacity: 1;
}
.column
{
display: flex;
flex-direction: column;
margin: 20px 10px;
color: #fff;
flex: 1;
.groupTitle
{
line-height: 20px;
max-height: 20px;
}
> *
{
display: flex;
height: 100%;
}
ul
{
display: flex;
flex-direction: column;
margin: 20px 0 0 10px;
padding: 10px 20px;
list-style: none;
border-left: 2px rgba(255,255,255,.1) solid;
li
{
position: relative;
flex: 1;
.title
{
font-size: 16px;
display: block;
margin-bottom: 3px;
color: #ff80ab;
}
.info
{
font-size: .8em;
font-weight: light;
color: #aaa;
}
}
}
}
}
}
}

View File

@ -1 +0,0 @@
$pink: #ff4081;

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

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

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

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

@ -0,0 +1,58 @@
import { FindUsers } from '@/domain'
import { RootState } from '@/store'
import { BareActionContext, getStoreBuilder } from 'vuex-typex'
export interface UserState {
currentUser: Meikan.User | null
foundUsers: Meikan.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: Meikan.User[]) {
state.foundUsers = foundUsers
}
function setCurrentUser(state: UserState, currentUser: Meikan.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(): Meikan.User[] {
return getterUserList()
},
get currentUser(): Meikan.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 { GetUserAnime } from '@/domain'
import { RootState } from '@/store'
import { BareActionContext, getStoreBuilder } from 'vuex-typex'
export interface UserAnimeState {
userAnime: Meikan.UserAnime[]
}
const initialState: UserAnimeState = {
userAnime: [],
}
const builder = getStoreBuilder<RootState>().module('userAnime', initialState)
// Getters
const getterUserAnime = builder.read(function anime(state: UserAnimeState) {
return state.userAnime
})
// Mutations
function setAnime(state: UserAnimeState, animeList: Meikan.UserAnime[]) {
state.userAnime = animeList
}
// Action
async function fetchUserAnime(context: BareActionContext<UserAnimeState, RootState>, user: Meikan.User) {
const list = await GetUserAnime(user.id)
userAnimeState.setAnime(list)
}
function resetUserAnime(context: BareActionContext<UserAnimeState, RootState>) {
userAnimeState.setAnime([])
}
export const userAnimeState = {
get anime(): Meikan.UserAnime[] {
return getterUserAnime()
},
setAnime: builder.commit(setAnime),
fetchUserAnime: builder.dispatch(fetchUserAnime),
resetUserAnime: builder.dispatch(resetUserAnime),
}

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

@ -0,0 +1,38 @@
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
declare namespace Meikan {
export interface User {
id: number
name: string
}
export interface Anime {
id: number
title: string
type: string
episodes: number
state: string
rating: string
start_date: string
}
export interface DetailedAnime extends Anime {
end_date: string
genres: string[]
average_duration: number
anidb_id: number
myanimelist_id: number
}
export interface UserAnime {
anime: Anime
state: string
episode: number
rating?: number
hidden: boolean
recommend: boolean
}
}

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

@ -0,0 +1,95 @@
<template>
<md-table v-model="searched" md-card md-sort="id" md-sort-order="asc">
<md-table-toolbar>
<h1 class="md-title md-toolbar-section-start">{{ currentUser.name }}'s anime</h1>
<div class="md-toolbar-section-end">
<md-field md-clearable>
<md-input placeholder="Search by name..." v-model="search" @input="searchOnTable" />
</md-field>
<md-button
class="md-icon-button md-primary"
@click="refresh"
>
<md-icon>refresh</md-icon>
</md-button>
</div>
</md-table-toolbar>
<md-table-empty-state v-if="loading">
<md-progress-spinner class="md-accent" md-mode="indeterminate"></md-progress-spinner>
</md-table-empty-state>
<md-table-empty-state
v-else
md-label="No results"
md-description="It's a void"
/>
<md-table-row slot="md-table-row" slot-scope="{ item }">
<md-table-cell md-label="#" md-numeric md-sort-by="anime.id">{{ item.anime.id }}</md-table-cell>
<md-table-cell md-label="Title" md-sort-by="anime.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 {
public loading: boolean = false
public search: string = ''
public searched: Meikan.UserAnime[] = []
@user.Getter
private currentUser!: Meikan.User
@userAnime.Action
private resetUserAnime!: () => void
@userAnime.Action
private fetchUserAnime!: (user: Meikan.User) => Promise<Meikan.UserAnime[]>
@userAnime.Getter
private anime!: Meikan.UserAnime[]
private created() {
if (this.anime.length !== 0) {
this.searched = this.anime
return
}
this.loading = true
this.fetchUserAnime(this.currentUser)
.then(() => {
this.loading = false
this.searched = this.anime
})
}
private refresh() {
this.resetUserAnime()
this.loading = true
this.fetchUserAnime(this.currentUser)
.then(() => {
this.loading = false
})
}
private searchOnTable() {
if (this.search.length === 0) {
this.searched = this.anime
}
this.searched = this.anime.filter((item) => {
return item.anime.title
.toLowerCase()
.includes(this.search.toLowerCase())
})
}
}
</script>

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

@ -0,0 +1,67 @@
<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: Meikan.User) => void
@userStore.Action('findUsers')
private findUsers!: (name: string) => Promise<Meikan.User[]>
@userStore.Action('resetFoundUsers')
private resetFoundUsers!: () => void
@userStore.Getter('foundUsers')
private foundUsers!: Meikan.User[]
@userAnimeStore.Action
private resetUserAnime!: () => void
private onInput = _.debounce((name: string) => {
if (name === '') {
this.resetFoundUsers()
return
}
this.findUsers(name)
}, 200)
private onSelect(user: Meikan.User) {
this.setCurrentUser(user)
this.resetUserAnime()
this.$router.push({ name: 'userAnime' })
}
private created() {
this.resetFoundUsers()
}
}
</script>

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

View File

@ -1,37 +0,0 @@
let menus = document.querySelectorAll('a[data-group]'),
subMenu = document.getElementById("subMenu"),
activeSubMenuGroup = [
document.querySelector(`div.group[data-group]`),
document.querySelector(`a[data-group]`)
];
subMenuGroups = [];
function openMenu(event){
activeSubMenuGroup[0].classList.remove("open");
activeSubMenuGroup[1].classList.remove("open");
activeSubMenuGroup[0] = subMenuGroups[this.dataset.group];
activeSubMenuGroup[1] = this;
this.classList.add("open");
subMenu.classList.add("open");
activeSubMenuGroup[0].classList.add("open");
}
function keepMenuOpen(event){
subMenu.classList.add("open");
activeSubMenuGroup[1].classList.add("open")
}
function closeMenu(event){
subMenu.classList.remove("open");
activeSubMenuGroup[1].classList.remove("open");
}
subMenu.addEventListener("mouseenter", keepMenuOpen);
subMenu.addEventListener("mouseleave", closeMenu);
for(let m of menus){
subMenuGroups[m.dataset.group] = document.querySelector(`div.group[data-group="${m.dataset.group}"]`);
m.addEventListener("mouseenter", openMenu);
m.addEventListener("mouseleave", closeMenu);
}

View File

@ -1,24 +0,0 @@
{{ template "header" }}
<link rel="stylesheet" href="/static/css/animeList.css">
<div class="container">
<div class="search">
<div class="filters"></div>
<div class="cardList">
{{ range . }}
<div class="card">
<img src="http://s1.narvii.com/image/d67vdzixlgx6jmy35vg2dgomjfhalqtf_hq.jpg">
<div class="body">
<div class="info">
<div class="title">{{ .Title }}</div>
<div class="synopsis">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Inventore iure deleniti iste harum, nobis nesciunt aliquid temporibus aspernatur corrupti est. Non fugiat illum ullam rem harum tempore neque, magni architecto. Non natus est officia dolor, obcaecati autem odio molestias assumenda. Minus quae ducimus aut reprehenderit commodi, voluptatum quis nulla autem.</div>
</div>
<div class="action">
<button>read more</button>
</div>
</div>
</div>
{{ end }}
</div>
</div>
</div>
{{ template "footer" }}

View File

@ -1,2 +0,0 @@
{{ template "header" .}}
{{ template "footer" }}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html style="height: 100%">
<head>
<title>Meikan</title>
<!-- <link rel="stylesheet" type="text/css" href="css/base.css"> -->
</head>
<body style="height: 100%">
<div style="position: relative; top: 50%; text-align: center; transform: translateY(-50%);">
<img src="/img/construction.png" style="width: 500px; vertical-align: middle;">
<div style="margin-top: 50px; font-family: Arial; font-weight: bold; font-size: 40px; opacity: .87;">Under construction</div>
</div>
</body>
</html>

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",
]
}

21
tslint.json Normal file
View File

@ -0,0 +1,21 @@
{
"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,
"no-namespace": false,
"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