Commit cdb0f672 authored by Garderes Francois's avatar Garderes Francois

Initial commit

parents
Pipeline #6319 canceled with stages
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
/client/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Config files (password)
/config/config.json
# Kaori
Kaori currently gets all its data from the website `https://myanimelist.net`, but this could be replaced in the future either by an other source, or by creating its own anime database.
## Pages
### Home page
(NOTE: Some features are to be implemented on the website. They are marked with (TBI))
The home page is the first page that appears when the user enters the website.
(TBI) A carousel displays the latest series that are recommended on MAL main page
(TBI) After this, there is a list of the "watching" animes that have been updated (aka. when a new episode has been released). By clicking on the name of the anime, the user can see what episode(s) have been released
(TBI) Recommendations: based on the animes being watched and seen, a few other animes should be suggested.
### Watching page
The watching page displays a grid of the animes being watched (and not completed).
Every anime is displayed in a Jumbotron. The available information on this page are:
* The name of the anime
* Details of the anime
- Synopsis
- Genre
- Episodes (number and names)
- Score, rating, popularity
### Seen page
The seen page displays a grid of the animes that have been completely watched.
Every anime is displayed in a Jumbotron. The available information on this page are:
* The name of the anime
* Details of the anime
- Synopsis
- Genre
- Episodes (number and names)
- Score, rating, popularity
- Personal score & comments
## Future improvements
Kaori currently only deals with anime, but it could also support manga and lightnovels in the future.
\ No newline at end of file
# TODO list
## Back
* Adapter le controller _extractMAL_ pour qu'il envoie les données au front, afin d'afficher les résultats de la recherche (s'inspirer de ce qui a été fait pour _watching_)
* Gérer les différents comptes d'utilisateur, afin que la partie "identification" soit fonctionnelle
## Front
* Mettre en place les différentes sections en fonction du menu choisi
* Créer les composants pour chaque section (_watching, seen, search_)
* Corriger _watching_ pour pouvoir afficher les _animeDetails_
* Ajouter une navbar avec les infos sur le compte et un accès aux différents menus. Navbar présente sur toutes les pages
* Le "anime_id" de AnimeTile doit être inutile : vérifier (sans le fichier css) et supprimer si inutile
* Ajouter système d'identification et contenu réservé aux inscrits
## Design
* Remplacer le bouton _Search_ des barres de recherche par une image de loupe
* Remplacer le logo React
* Vérifier que le site s'affiche bien sur mobile
* Faire un CSS convenable pour que le site soit pas trop moche
* Proprifier les "Jumbotron" de l'AnimeTile : pour l'instant, la grille pour "Search", "Watching" (et bientôt "Seen") est moche
* Modifier le CSS de Modal pour que le 'background' ne soit pas noir
# Concept
## Search
* Quand on appuie sur le bouton "search", on obtient bien les résultats. Toutefois, j'aimerais que "appuyer sur Entrée" et "cliquer sur 'search'" soient deux actions équivalentes, ce qui n'est pas le cas (appuyer sur entrer recharge la page, mais ne donne pas les résultats)
* Il faut un autre modèle d'AnimeTile, et ce pour deux raisons. La première est qu'il faut certaines informations supplémentaires, notamment le synopsis et le ranking (deux éléments très importants). La deuxième est qu'il faut rajouter le bouton "Add" pour rajouter l'anime dans la base de données (<=> ajouter l'anime à 'Watching'). Voir si on peut juste créer AnimeTileSearch et lui faire hériter la classe AnimeTile, ce qui permettrait de gagner du temps (NOTE : apparemment, l'hérédité n'est pas vraiment optimale pour React)
Au lieu d'en créer un autre, on peut simplement faire passer une variable "type" et modifier ensuite les animeDetails en fonction du "type" passé en props => implémenter cette dernière solution
* (Optionnel) La recherche d'informations sur les animes prend trop de temps. Il faudrait accélérer tout ça. Une idée : parser le bloc de code html correspondant à toutes les infos sur les animes, et à partir de ce bloc, récupérer les infos. Ainsi, au lieu de parser plusieurs fois une même page et le gros bloc de code associé, on peut effectuer nos recherches au sein d'un bloc plus réduit, sans avoir besoin de parser à nouveau (pas de nouvelle recherche internet, seulement calcul du PC)
## Watching
* Ajouter un "Progress" (cf. react bootstrap), pour afficher l'avancée du visionnage (nombre d'épisodes vus / nombre total)
* Ajouter une "checkbox" pour marquer les épisodes déjà vus (remplacer le 'ListGroupItem' par un 'CustomComponent'). Il faudra donc sauvegarder dans la bdd les épisodes déjà vus/cochés.
## Seen
## A faire pour le DTY
* Faire un système de connexion avec plusieurs utilisateurs
* Si possible, s'occuper des épisodes à cocher/mémoriser, à l'aide de la fonction "changeDetailsAnime" dans 'animesWatching.controller.js'
* Supprimer le fichier 'config' du gitlab
\ No newline at end of file
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "kaori",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"react": "^16.4.1",
"react-bootstrap": "^0.32.1",
"react-dom": "^16.4.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:5000/"
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import AlertConnexion from "./components/ConnexionInitiale.js";
import TabsSections from "./components/sections/Sections.js";
import KaoriNavBar from "./components/kaoriNavBar/KaoriNavBar.js";
// Si l'utilisateur est connecté, afficher les menus "Watching" et "Seen"
// Sinon, laisser les champs vides jusqu'à ce que l'utilisateur se connecte
class App extends React.Component {
state = {
response: "",
sectionKey: 1,
};
componentDidMount() {
this.callApi()
.then(res => this.setState({ response: res.express }))
.catch(err => console.log(err));
}
callApi = async () => {
const response = await fetch("/api/hello");
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
};
handleSectionChange = (section) => {
this.setState({sectionKey: section});
}
render() {
const isLoggedIn = this.state.response.user;
let alert;
if (isLoggedIn === "?") {
alert = <AlertConnexion />;
} else {
alert = <p> Bienvenue {this.state.response.user} ! </p>;
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Kaori</h1>
</header>
<div className="AlerteConnexion">{alert}</div>
<p className="App-intro">{this.state.response.message}</p>
<TabsSections onSectionChange={this.handleSectionChange}/>
</div>
);
}
}
// Display the search bar only for : Search, Watching, Seen
// It has a different role in Search and in Watching/Seen (search on the internet or in the DB)
export default App;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
/*
import React from "react";
// Fonction pour récupérer URL de mangoDB
import getDbConnectionString from "../../../config/index";
class GuestBook extends Component {
constructor(props) {
super(props);
this.handleAnimeDetails = this.handleAnimeDetails.bind(this);
this.state = { AnimeDetails: "" };
}
handleAnimeDetails(event) {
this.setState({ AnimeDetails: event.target.value });
}
addToWatchingAnime = event => {
event.preventDefault();
this.setState({
AnimeDetails: event.target.value
});
};
/*
axios.post(getDbConnectionString(), { AnimeDetails: this.state.AnimeDetails })
.then(response => {
console.log(response, 'Anime added!');
})
.catch(err => {
console.log(err, 'Anime not added, try again');
});
this.setState({
AnimeDetails: ""
});
}
*/
import React from "react";
import { Button, Alert } from "react-bootstrap";
import ConnexionForm from "./seConnecter/components/FormConnexion.js";
import ConnexionWindow from "./seConnecter/SeConnecter.js";
class AlertConnexion extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
show: true,
shouldRenderConnexionForm: false
};
}
handleDismiss = () => {
this.setState({ show: false });
};
handleShow = () => {
this.setState({ show: true });
};
handleClick = () => {
this.setState(prevState => ({
shouldRenderConnexionForm: !prevState.shouldRenderConnexionForm
}));
};
render() {
if (this.state.show) {
return (
<Alert bsStyle="danger" onDismiss={this.handleDismiss}>
<h4> Vous n'êtes pas encore connecté !</h4>
<p>
<Button bsStyle="danger" onClick={this.handleClick}>
{" "}
Connexion{" "}
</Button>
<span> ou </span>
<Button onClick={this.handleDismiss}> Continuer </Button>
{this.state.shouldRenderConnexionForm && <ConnexionForm />}
</p>
</Alert>
);
}
return <Button onClick={this.handleShow}>Show Alert</Button>;
}
}
export default AlertConnexion;
import React from "react";
import { Jumbotron, Button, Col, Modal } from "react-bootstrap";
import AnimeDetails from "./components/AnimeDetails";
import axios from "axios";
class AnimeTile extends React.Component {
constructor(props) {
super(props);
this.state = {
show: false,
showRemove: false,
type: this.props.type,
buttonText: "",
};
}
onHover = () => {
// When the mouse hovers the picture, the picture should become gray and the title should appear
// This would be more enjoyable than just a dull button with the title
}
handleClose = () => {
this.setState({ show: false });
};
handleShow = () => {
this.setState({ show: true });
};
handleCloseRemove = () => {
this.setState({ showRemove: false });
};
handleShowRemove = () => {
this.setState({ showRemove: true });
};
addShow = () => {
axios
.get(`/watching/addToWatching/${this.props.anime.numberURL}/${this.props.anime.endingURL}`)
.then(res => {
this.setState({ type: "watching" });
window.location.reload();
})
.catch(function(err) {
console.log(err);
});
};
removeShow = () => {
axios
.get(`/watching/delete/${this.props.anime._id}`)
.then(res => {
this.handleCloseRemove();
window.location.reload();
})
.catch(function(err) {
console.log(err);
});
};
rewatchShow = () => {
axios
.get(`/seen/changeDetails/sendWatching/${this.props.anime._id}`)
.then(res => {
this.handleCloseRemove();
window.location.reload();
})
.catch(function(err) {
console.log(err);
});
};
render() {
const type = this.props.type;
let button, buttonConfirm;
if(type === "search") {
button = <Button bsStyle="default" onClick={this.addShow}>Add</Button>
}
else if(type === "watching") {
button = <Button bsStyle="primary" onClick={this.handleShowRemove}>Remove</Button>;
buttonConfirm = <Button bsStyle="default" onClick={this.removeShow}>Remove</Button>;
}
else {
button = <Button bsStyle="default" onClick={this.handleShowRemove}>Rewatch</Button>
buttonConfirm = <Button bsStyle="default" onClick={this.rewatchShow}>Rewatch</Button>;
} // Cas 'else' <=> cas 'seen'
return (
<div>
<Col sm={{size:'auto'}} md={{size: 'auto', offset: 2}}>
<Jumbotron>
<img src={this.props.anime.picture} />
<h4>{this.props.anime.title}</h4>
<Button bsStyle="primary" onClick={this.handleShow}>
Infos
</Button>
{button}
<div className="static-modal">
<Modal show={this.state.showRemove} onHide={this.handleCloseRemove} animation={false}>
<Modal.Dialog>
<Modal.Header>
<Modal.Title>Confirmation</Modal.Title>
</Modal.Header>
<Modal.Body>Are you sure you want to remove this anime from your '{this.props.type}' list?</Modal.Body>
<Modal.Footer>
<Button onClick={this.handleCloseRemove}>Cancel</Button>
{buttonConfirm}
</Modal.Footer>
</Modal.Dialog>
</Modal>
</div>
<AnimeDetails anime={this.props.anime} show={this.state.show} onClosedModal={this.handleClose} type={this.props.type}/>
</Jumbotron>
</Col>
</div>
);
}
}
export default AnimeTile;
\ No newline at end of file
import React from "react";
import { Button, Modal } from "react-bootstrap";
import axios from "axios";
import EpisodesCheck from "./episodesCheck";
class AnimeDetails extends React.Component {
constructor(props) {
super(props);
this.state = {
show: false
};
}
handleClose = () => {
this.setState({show: false});
this.props.onClosedModal();
}
sendWatchingSeen = () => {
axios
.post(`/watching/changeDetails/sendSeen/${this.props.anime._id}`)
.then(res => {
this.handleClose();
window.location.reload();
})
.catch(function(err) {
console.log(err);
});
}
render() {
let buttonSend;
if(this.props.type === "watching") {
buttonSend = <Button bsStyle="primary" onClick={this.sendWatchingSeen}>Seen</Button>;
}
return (
<div className="anime-details">
<Modal show={this.props.show} onHide={this.handleClose} animation={false}>
<Modal.Header closeButton>
<Modal.Title>
<h1>{this.props.anime.title}</h1>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>Synopsis</h4>
<p>{this.props.anime.synopsis}</p>
<p>Premiered: {this.props.anime.premiered}</p>
<p>Status: {this.props.anime.status}</p>
<p>Studios: {this.props.anime.studios}</p>
<p>Genres: {this.props.anime.genres}</p>
<p>Rating: {this.props.anime.rating}</p>
<p>Ranked: {this.props.anime.ranked}</p>
<p>Popularity: {this.props.anime.popularity}</p>
<p>Nature: {this.props.anime.nature}</p>
<p>Score: {this.props.anime.score}</p>
<h4>Episodes : </h4>
<p><EpisodesCheck anime={this.props.anime} numberURL={this.props.anime.numberURL} endingURL={this.props.anime.endingURL}/></p>
</Modal.Body>
<Modal.Footer>
{buttonSend}
<Button onClick={this.handleClose}>Fermer </Button>
</Modal.Footer>
</Modal>
</div>
);
}
}
export default AnimeDetails;
\ No newline at end of file
import React from "react";
import { ListGroup, ListGroupItem, Checkbox } from "react-bootstrap";
import axios from "axios";
class EpisodesCheck extends React.Component {
constructor(props) {
super(props);
this.state = {
listNameEpisodes : [],
listWatchProgress: [],
}
}
componentDidMount() {
this.setState({listWatchProgress: this.props.anime.episodeSeen});
axios.get(`extractMAL/searchAnime/${this.props.numberURL}/${this.props.endingURL}/episodesNames`)
.then(res => {
this.setState({listNameEpisodes : res.data});
// Then we'll have to update the results in the parent component (animeDetails)
})
.catch(function(err) {
console.log(err);
});
}
handleCheck = (event) => {
let index = event.target.value;
let listNewWatch = this.state.listWatchProgress;
listNewWatch[index] = !this.state.listWatchProgress[index];
this.setState({listWatchProgress: listNewWatch});
// Fix for axios POST : normal syntax doesn't work, so had to add the 'params' variable
const params = new URLSearchParams();
params.append('listProgress', this.state.listWatchProgress);
axios.post(`watching/changeDetails/${this.props.anime._id}`, params)
// And now, we update the value of each episode seen (later, we can replace this by a 'Save changes' button, to save only once)
.then(res => {
console.log("Success!");
})
.catch(function(err) {
console.log(err);
});
};
render() {
return (
<div className="anime-details-episodes">
<ListGroup>
{this.state.listNameEpisodes.map((episode, index) => (
<ListGroupItem header={episode.episodeTitle}>
{episode.aired}
<input
className="episode_checkbox"
type="checkbox"
value={index}
onChange={this.handleCheck}
checked={this.state.listWatchProgress[index]}
/>
</ListGroupItem>
))}
</ListGroup>
</div>
);
}
}
export default EpisodesCheck;