Compare commits

...

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

43 changed files with 1318 additions and 18773 deletions

View File

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

27
.gitignore vendored
View File

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

View File

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

72
api/api.go Normal file
View File

@ -0,0 +1,72 @@
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
}

View File

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

5
components/footer.gohtml Normal file
View File

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

260
components/header.gohtml Normal file
View File

@ -0,0 +1,260 @@
{{ 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 Normal file
View File

@ -0,0 +1,79 @@
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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,20 +0,0 @@
<!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>

74
sass/animeList.scss Normal file
View File

@ -0,0 +1,74 @@
@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;
}
}
}
}
}
}

217
sass/base.scss Normal file
View File

@ -0,0 +1,217 @@
@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;
}
}
}
}
}
}
}
}

159
sass/menu.scss Normal file
View File

@ -0,0 +1,159 @@
.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;
}
}
}
}
}
}
}

1
sass/variables.scss Normal file
View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,19 +0,0 @@
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
}

View File

@ -1,29 +0,0 @@
<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>

View File

@ -1,18 +0,0 @@
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')

View File

@ -1,32 +0,0 @@
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,
},
],
},
],
})

View File

@ -1,27 +0,0 @@
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

View File

@ -1,58 +0,0 @@
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

@ -1,43 +0,0 @@
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),
}

View File

@ -1,38 +0,0 @@
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
}
}

View File

@ -1,95 +0,0 @@
<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>

View File

@ -1,67 +0,0 @@
<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.

BIN
static/fonts/roboto.woff2 Normal file

Binary file not shown.

362
static/img/bg.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

BIN
static/img/construction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

37
static/js/menu.js Normal file
View File

@ -0,0 +1,37 @@
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);
}

24
templates/anime.gohtml Normal file
View File

@ -0,0 +1,24 @@
{{ 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" }}

2
templates/home.gohtml Normal file
View File

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

13
templates/index.gohtml Normal file
View File

@ -0,0 +1,13 @@
<!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>

View File

@ -1,40 +0,0 @@
{
"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",
]
}

View File

@ -1,21 +0,0 @@
{
"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"]
}
}

View File

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

7564
yarn.lock

File diff suppressed because it is too large Load Diff