Merge branch 'master' into 'production'

Master

See merge request Romulus21/portal!36
This commit is contained in:
Romain Delanoë
2020-04-30 21:38:23 +00:00
52 changed files with 1398 additions and 804 deletions

View File

@@ -6,26 +6,22 @@
},
"extends": [
"eslint:recommended",
"plugin:vue/essential",
"plugin:vue/base",
"plugin:vue/strongly-recommended",
"plugin:vue/recommended"
"plugin:vue/essential"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
"ecmaVersion": 2018
},
"plugins": [
"vue"
],
"rules": {
"indent": [
"off",
2
"error",
4
],
"linebreak-style": [
"error",
@@ -36,10 +32,8 @@
"single"
],
"semi": [
"off",
"always"
],
"no-console": "off",
"strict": "off"
"error",
"never"
]
}
}

View File

@@ -14,6 +14,10 @@ class ToDoController extends Controller
{
$this->authorize('create', ToDoList::class);
$lastToDo = $toDoList->toDos()->orderBy('order', 'desc')->first();
request()['order'] = ($lastToDo) ? $lastToDo->order + 1 : 1;
$toDo = $toDoList->toDos()->create($this->validateData());
return (new ToDoResource($toDo))
@@ -32,10 +36,34 @@ class ToDoController extends Controller
->setStatusCode(200);
}
public function destroy(ToDoList $toDoList, ToDo $toDo)
{
$this->authorize('delete', $toDoList);
$toDo->delete();
return response([], 204);
}
public function changeOrder(ToDoList $toDoList, ToDo $toDo)
{
$this->authorize('update', $toDoList);
$toDoChanged = $toDoList->toDos()->where('order', (int) request()['new-order'])->first();
$toDoChanged->update(['order' => (int) $toDo->order]);
$toDo->update(['order' => (int) request()['new-order']]);
return (new ToDoResource($toDo))
->response()
->setStatusCode(200);
}
private function validateData()
{
return request()->validate([
'name' => 'required',
'order' => 'integer|min:1|max:1000000',
]);
}
}

View File

@@ -8,6 +8,13 @@ use Illuminate\Http\Request;
class ToDoListController extends Controller
{
public function index()
{
$this->authorize('viewAny', ToDoList::class);
return ToDoListResource::collection(request()->user()->toDoLists);
}
public function store()
{
$this->authorize('create', ToDoList::class);

View File

@@ -21,7 +21,7 @@ class ToDo extends JsonResource
'attributes' => [
'data' => [
'name' => $this->name,
'order' => $this->order,
'order' => (int) $this->order,
'checked_at' => optional($this->checked_at)->diffForHumans(),
'last_updated' => $this->updated_at->diffForHumans(),
]

View File

@@ -23,7 +23,7 @@ class ToDoList extends JsonResource
'data' => [
'name' => $this->name,
'to_dos' => new ToDoCollection($this->toDos),
'posted_by' => new UserResource($this->user),
'posted_by' => new UserResource($this->author),
'last_updated' => $this->updated_at->diffForHumans(),
// 'cover_image' => new ImageResource($this->coverImage),
]

View File

@@ -16,7 +16,7 @@ class ToDoList extends Model
return '/to-do-lists/' . $this->id;
}
public function users(): BelongsTo
public function author(): BelongsTo
{
return $this->belongsTo(User::class);
}

View File

@@ -59,6 +59,11 @@ class User extends Authenticatable
return $this->hasMany(Memo::class);
}
public function toDoLists() : HasMany
{
return $this->hasMany(ToDoList::class);
}
public function images(): MorphMany
{
return $this->morphMany(Image::class, 'imageable');
@@ -83,9 +88,4 @@ class User extends Authenticatable
$userImage->path = 'images/default-cover.jpg';
});
}
public function toDoLists(): HasMany
{
return $this->hasMany(ToDoList::class);
}
}

View File

@@ -0,0 +1,16 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Models\ToDo;
use Faker\Generator as Faker;
$factory->define(ToDo::class, function (Faker $faker) {
$toDoList = factory(\App\Models\ToDoList::class);
return [
'to_do_list_id' => $toDoList,
'name' => $faker->words(3, [false]),
// 'order' => $toDoList->toDos->orderBy('id', 'desc')->first()->order + 1,
// 'checket_at' => now(),
];
});

View File

@@ -17,6 +17,7 @@ class CreateToDoListsTable extends Migration
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('name');
$table->integer('order')->default(1);
$table->timestamps();
});
}

View File

@@ -17,7 +17,7 @@ class CreateToDosTable extends Migration
$table->id();
$table->unsignedBigInteger('to_do_list_id');
$table->string('name');
$table->integer('order')->default(0);
$table->integer('order')->default(1);
$table->timestamp('checked_at')->nullable();
$table->timestamps();
});

View File

@@ -8,14 +8,20 @@
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"eslint": "./node_modules/.bin/eslint resources/assets/js/ test/ --ext .js,.vue"
"eslint": "./node_modules/.bin/eslint resources/js/ --ext .js,.vue",
"lint": "eslint --ext .js,.vue resources/js/"
},
"devDependencies": {
"axios": "^0.19",
"babel-eslint": "^10.1.0",
"cross-env": "^7.0",
"eslint": "^6.8.0",
"eslint-config-standard": "^14.1.1",
"eslint-loader": "^3.0.3",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.2.2",
"laravel-mix": "^5.0.1",
"laravel-mix-eslint": "^0.1.3",

9
resources/js/app.js vendored
View File

@@ -2,12 +2,13 @@ import Vue from 'vue'
import router from './router'
import App from './components/App'
import store from './store'
import SvgVue from 'svg-vue';
import SvgVue from 'svg-vue'
Vue.use(SvgVue);
Vue.use(SvgVue)
require('./bootstrap');
require('./bootstrap')
// eslint-disable-next-line no-unused-vars
const app = new Vue({
el: '#app',
@@ -15,4 +16,4 @@ const app = new Vue({
App
},
router, store,
});
})

View File

@@ -1,7 +1,5 @@
window._ = require('lodash');
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window._ = require('lodash')
window.axios = require('axios')
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'

View File

@@ -8,19 +8,19 @@
</template>
<script>
export default {
name: "AlertBox",
props: {
type: {
type: String,
required: true,
default: 'success',
},
message: {
type: String,
required: true,
default: 'message',
},
}
export default {
name: 'AlertBox',
props: {
type: {
type: String,
required: true,
default: 'success',
},
message: {
type: String,
required: true,
default: 'message',
},
}
}
</script>

View File

@@ -11,30 +11,31 @@
</template>
<script>
import Nav from "./Nav";
import TopBar from "./TopBar";
import { mapGetters } from 'vuex'
import Nav from './Nav'
import TopBar from './TopBar'
import { mapGetters } from 'vuex'
export default {
name: "App",
components : {
Nav, TopBar
},
mounted() {
this.$store.dispatch('fetchAuthUser')
},
created() {
this.$store.dispatch('setPageTitle', this.$route.meta.title)
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
watch: {
$route(to, from) {
this.$store.dispatch('setPageTitle', to.meta.title)
}
export default {
name: 'App',
components : {
Nav, TopBar
},
mounted() {
this.$store.dispatch('fetchAuthUser')
},
created() {
this.$store.dispatch('setPageTitle', this.$route.meta.title)
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
watch: {
// eslint-disable-next-line no-unused-vars
$route(to, from) {
this.$store.dispatch('setPageTitle', to.meta.title)
}
}
}
</script>

View File

@@ -7,8 +7,8 @@
</template>
<script>
export default {
name: "Avatar",
props: ['avatar', 'alt', 'size']
}
export default {
name: 'Avatar',
props: ['avatar', 'alt', 'size']
}
</script>

View File

@@ -1,68 +1,69 @@
<template>
<div class="relative mb-2">
<label :for="name" class="pb-2 font-bold text-xl ml-1">{{ label }}</label>
<input :id="name" :type="type" :placeholder="placeholder" v-model="value" @input="updateField()" :class="errorClassObject()" class="w-full rounded p-2">
<label v-if="label" :for="name" class="pb-2 font-bold text-xl ml-1">{{ label }}</label>
<input :id="name" :type="type" :placeholder="placeholder" v-model="value" @input="updateField()" :class="'w-full rounded p-2 ' + classes + ' ' + errorClassObject()">
<p class="text-red-600 m-0" v-text="errorMessage()">Error Here</p>
</div>
</template>
<script>
export default {
name: "InputField",
props: {
name: {
type: String,
required: true
},
type: {
type: String,
default: 'text'
},
label: String,
placeholder: String,
required: {
type: Boolean,
default: false
},
errors: Object,
data: String,
export default {
name: 'InputField',
props: {
name: {
type: String,
required: true
},
data: function () {
type: {
type: String,
default: 'text'
},
label: String,
placeholder: String,
required: {
type: Boolean,
default: false
},
errors: Object,
data: String,
classes: String,
},
data: function () {
return {
value: ''
}
},
computed: {
hasError: function () {
return this.required && this.errors && this.errors[this.name] && this.errors[this.name].length > 0
}
},
methods: {
updateField: function () {
this.clearErrors(this.name)
this.$emit('update:field', this.value)
},
errorMessage: function () {
if (this.hasError) {
return this.errors[this.name][0]
}
},
clearErrors: function () {
if (this.hasError) {
this.errors[this.name] = null
}
},
errorClassObject: function () {
return {
value: ''
}
},
computed: {
hasError: function () {
return this.required && this.errors && this.errors[this.name] && this.errors[this.name].length > 0
}
},
methods: {
updateField: function () {
this.clearErrors(this.name);
this.$emit('update:field', this.value)
},
errorMessage: function () {
if (this.hasError) {
return this.errors[this.name][0]
}
},
clearErrors: function () {
if (this.hasError) {
this.errors[this.name] = null
}
},
errorClassObject: function () {
return {
'error-field': this.hasError
}
}
},
watch: {
data: function (val) {
this.value = val
'error-field': this.hasError
}
}
},
watch: {
data: function (val) {
this.value = val
}
}
}
</script>

View File

@@ -5,15 +5,15 @@
</template>
<script>
export default {
name: "Loader",
data: function () {
return {
loading: false,
}
},
mounted() {
setTimeout(() => this.loading = true, 250)
export default {
name: 'Loader',
data: function () {
return {
loading: false,
}
},
mounted() {
setTimeout(() => this.loading = true, 250)
}
}
</script>

View File

@@ -7,11 +7,15 @@
<hr>
<router-link to="/memos" class="nav-item p-2">
<svg-vue icon="memos" />
<span v-bind:class="{ navhidden: !toggleNav }">Memos</span>
<span>Memos</span>
</router-link>
<router-link to="/to-do-lists" class="nav-item p-2">
<svg-vue icon="list" />
<span>ToDo Lists</span>
</router-link>
<router-link to="/jeux" class="nav-item p-2">
<svg-vue icon="games" />
<span v-bind:class="{ navhidden: !toggleNav }">Jeux</span>
<span>Jeux</span>
</router-link>
</div>
<div @click="toggleNavBar" class="nav-toggle flex ml-2">
@@ -21,22 +25,22 @@
</template>
<script>
export default {
name: "Nav",
data: function () {
return {
toggleNav: true,
}
},
mounted() {
let isTrueSet = (localStorage.getItem('navbar') === 'true');
(isTrueSet) ? this.toggleNav = true : this.toggleNav = false
},
methods: {
toggleNavBar() {
this.toggleNav = !this.toggleNav
localStorage.setItem('navbar', JSON.stringify(this.toggleNav));
}
export default {
name: 'Nav',
data: function () {
return {
toggleNav: true,
}
},
mounted() {
let isTrueSet = (localStorage.getItem('navbar') === 'true');
(isTrueSet) ? this.toggleNav = true : this.toggleNav = false
},
methods: {
toggleNavBar(){
this.toggleNav = !this.toggleNav
localStorage.setItem('navbar', JSON.stringify(this.toggleNav))
}
}
}
</script>

View File

@@ -1,67 +1,74 @@
<template>
<div class="relative mb-2">
<label v-if="label" :for="name" class="pb-1">{{ label }}</label>
<textarea :id="name" type="text" v-model="value" :placeholder="placeholder" @input="updateField()" :class="errorClassObject()" class="w-full h-64 rounded p-2">{{ data }}</textarea>
<textarea :id="name"
type="text"
v-model="value"
:placeholder="placeholder"
@input="updateField()"
:class="errorClassObject()"
class="w-full h-64 rounded p-2">
</textarea>
<p class="text-red-600 m-0" v-text="errorMessage()">Error Here</p>
</div>
</template>
<script>
export default {
name: "TextAreaField",
props: {
name: {
type: String,
required: true
},
type: {
type: String,
default: 'text'
},
label: String,
placeholder: String,
required: {
type: Boolean,
default: false
},
errors: Object,
data: String,
export default {
name: 'TextAreaField',
props: {
name: {
type: String,
required: true
},
data: function () {
type: {
type: String,
default: 'text'
},
label: String,
placeholder: String,
required: {
type: Boolean,
default: false
},
errors: Object,
data: String,
},
data: function () {
return {
value: ''
}
},
computed: {
hasError: function () {
return this.required && this.errors && this.errors[this.name] && this.errors[this.name].length > 0
}
},
methods: {
updateField: function () {
this.clearErrors(this.name)
this.$emit('update:field', this.value)
},
errorMessage: function () {
if (this.hasError) {
return this.errors[this.name][0]
}
},
clearErrors: function () {
if (this.hasError) {
this.errors[this.name] = null
}
},
errorClassObject: function () {
return {
value: ''
}
},
computed: {
hasError: function () {
return this.required && this.errors && this.errors[this.name] && this.errors[this.name].length > 0
}
},
methods: {
updateField: function () {
this.clearErrors(this.name);
this.$emit('update:field', this.value)
},
errorMessage: function () {
if (this.hasError) {
return this.errors[this.name][0]
}
},
clearErrors: function () {
if (this.hasError) {
this.errors[this.name] = null
}
},
errorClassObject: function () {
return {
'error-field': this.hasError
}
}
},
watch: {
data: function (val) {
this.value = val
'error-field': this.hasError
}
}
},
watch: {
data: function (val) {
this.value = val
}
}
}
</script>

View File

@@ -20,35 +20,23 @@
</template>
<script>
import { mapGetters } from 'vuex'
import Avatar from "./Avatar";
import { mapGetters } from 'vuex'
import Avatar from './Avatar'
export default {
name: "TopBar",
components: {
Avatar
},
data: function () {
return {
search: false,
}
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
methods: {
logout: function () {
axios.post('logout')
.then(res => {
if(res.status ===302 || 401) {
window.location.href = '/login'
}
}).catch(error => {
})
}
export default {
name: 'TopBar',
components: {
Avatar
},
data: function () {
return {
search: false,
}
},
computed: {
...mapGetters({
authUser: 'authUser',
})
}
}
</script>

View File

@@ -8,80 +8,80 @@
</template>
<script>
import Dropzone from 'dropzone'
import { mapGetters } from 'vuex'
import Dropzone from 'dropzone'
import { mapGetters } from 'vuex'
export default {
name: "UploadableImage",
props: {
imageWidth: {
type: Number,
required: true,
},
imageHeight: {
type: Number,
required: true,
},
location: {
type: String,
required: true
},
image: {
type: Object,
default: null,
},
author: {
type: Object,
required: true
},
id: {
type: Number,
required: true
},
model: {
type: String,
required: true
},
classes: String,
alt: String
export default {
name: 'UploadableImage',
props: {
imageWidth: {
type: Number,
required: true,
},
data: () => {
imageHeight: {
type: Number,
required: true,
},
location: {
type: String,
required: true
},
image: {
type: Object,
default: null,
},
author: {
type: Object,
required: true
},
id: {
type: Number,
required: true
},
model: {
type: String,
required: true
},
classes: String,
alt: String
},
data: () => {
return {
dropzone: null,
dropImage: null,
}
},
mounted() {
if(this.authUser.data.user_id === this.author.data.user_id) {
this.dropzone = new Dropzone(this.$refs.image, this.settings)
}
},
computed: {
...mapGetters({
authUser: 'authUser'
}),
settings() {
let url = '/api/images/' + this.model + '/' + this.id
return {
dropzone: null,
dropImage: null,
}
},
mounted() {
if(this.authUser.data.user_id === this.author.data.user_id) {
this.dropzone = new Dropzone(this.$refs.image, this.settings)
}
},
computed: {
...mapGetters({
authUser: 'authUser'
}),
settings() {
let url = '/api/images/' + this.model + '/' + this.id
return {
paramName: 'image',
url: url,
acceptedFiles: 'image/*',
params: {
'width': this.imageWidth,
'height': this.imageHeight,
'location': this.location,
},
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name=csrf-token]').content
},
success: (e, res) => {
this.dropImage = res
}
paramName: 'image',
url: url,
acceptedFiles: 'image/*',
params: {
'width': this.imageWidth,
'height': this.imageHeight,
'location': this.location,
},
headers: {
'X-CSRF-TOKEN': document.head.querySelector('meta[name=csrf-token]').content
},
success: (e, res) => {
this.dropImage = res
}
},
imageObject() {
return this.dropImage || this.image
}
},
imageObject() {
return this.dropImage || this.image
}
}
}
</script>

View File

@@ -1,16 +1,18 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from "./views/Home"
import Profile from "./views/User/ProfileUser";
import ShowUser from "./views/User/ShowUser";
import DashBoard from "./views/DashBoard";
import CssTesteur from "./views/CssTesteur";
import MemoIndex from "./views/Memo/MemoIndex";
import MemoCreate from "./views/Memo/MemoCreate";
import MemoShow from "./views/Memo/MemoShow";
import MemoEdit from "./views/Memo/MemoEdit";
import GameIndex from "./views/Games/GameIndex";
import Hangman from "./views/Games/HangMan/Hangman";
import Home from './views/Home'
import Profile from './views/User/ProfileUser'
import ShowUser from './views/User/ShowUser'
import DashBoard from './views/DashBoard'
import CssTesteur from './views/CssTesteur'
import MemoIndex from './views/Memo/MemoIndex'
import MemoCreate from './views/Memo/MemoCreate'
import MemoShow from './views/Memo/MemoShow'
import MemoEdit from './views/Memo/MemoEdit'
import ToDoListIndex from './views/ToDoLists/ToDoListIndex'
import ToDoListShow from './views/ToDoLists/ToDoListShow'
import GameIndex from './views/Games/GameIndex'
import Hangman from './views/Games/HangMan/Hangman'
Vue.use(VueRouter)
@@ -54,6 +56,14 @@ export default new VueRouter({
meta: {title: 'Edit Memo'}
},
{
path: '/to-do-lists', component: ToDoListIndex,
meta: {title: 'To Do Lists'}
}, {
path: '/to-do-lists/:id', component: ToDoListShow,
meta: {title: 'Details of List'}
},
{
path: '/jeux', component: GameIndex,
meta: {title: 'Liste des jeux'}

View File

@@ -10,13 +10,15 @@ const getters = {
}
const actions = {
// eslint-disable-next-line no-unused-vars
fetchAuthUser({commit, state}) {
// eslint-disable-next-line no-undef
axios.get('/api/auth-user')
.then(res => {
commit('setAuthUser', res.data)
})
.catch(error => {
console.log('Unable to fetch auth user')
console.log('Unable to fetch auth user' + error)
})
}
}

View File

@@ -37,7 +37,7 @@
</template>
<script>
export default {
name: "CssTesteur"
}
export default {
name: 'CssTesteur'
}
</script>

View File

@@ -11,12 +11,26 @@
</template>
<script>
import UserAdmin from "./User/UserAdmin";
import UserAdmin from './User/UserAdmin'
export default {
name: "DashBoard",
components: {
UserAdmin
export default {
name: 'DashBoard',
components: {
UserAdmin
},
methods: {
logout: function () {
// eslint-disable-next-line no-undef
axios.post('logout')
.then(res => {
// eslint-disable-next-line no-constant-condition
if(res.status === 302 || 401) {
window.location.href = '/login'
}
}).catch(() => {
})
}
}
}
</script>

View File

@@ -8,12 +8,12 @@
</template>
<script>
import HangmanIndex from "./HangMan/HangmanIndex";
import HangmanIndex from './HangMan/HangmanIndex'
export default {
name: "GameIndex",
components: {
HangmanIndex
}
export default {
name: 'GameIndex',
components: {
HangmanIndex
}
}
</script>

View File

@@ -3,7 +3,7 @@
<h1 class="text-3xl text-center text-orange-900 font-bold">Pendu</h1>
<p class="text-center mb-4">Trouve le mot du pendu - Saisi les lettres</p>
<div class="flex justify-center dic-select">
<div v-for="(dic, key) in dictionary">
<div v-for="(dic, key) in dictionary" :key="key">
<a v-if="!selectedWord" class="btn-primary mr-1" @click="selectWord">{{ key }}</a>
</div>
</div>
@@ -46,116 +46,116 @@
</template>
<script>
import json from './dict.json'
import json from './dict.json'
export default {
name: "Hangman",
data: function () {
return {
dictionary: json,
letterBox: '',
figureParts: [],
correctLetters: [],
wrongLetters: [],
wrongLettersEl: '',
selectedWord: '',
wordEl: '',
finalMessage: '',
popup: false,
notification: false,
}
export default {
name: 'Hangman',
data: function () {
return {
dictionary: json,
letterBox: '',
figureParts: [],
correctLetters: [],
wrongLetters: [],
wrongLettersEl: '',
selectedWord: '',
wordEl: '',
finalMessage: '',
popup: false,
notification: false,
}
},
mounted() {
this.figureParts = this.$el.querySelectorAll('.figure-part')
window.addEventListener('keyup', this.keyMessage)
},
methods: {
selectWord(e) {
this.selectedWord = this.dictionary[e.target.textContent][Math.floor(Math.random() * this.dictionary[e.target.textContent].length)].toLowerCase()
this.displayWord()
this.popup = false
},
mounted() {
this.figureParts = this.$el.querySelectorAll('.figure-part')
window.addEventListener('keyup', this.keyMessage)
},
methods: {
selectWord(e) {
this.selectedWord = this.dictionary[e.target.textContent][Math.floor(Math.random() * this.dictionary[e.target.textContent].length)].toLowerCase()
this.displayWord()
this.popup = false
},
keyMessage(e) {
if(e.keyCode >= 65 && e.keyCode <= 90) {
const letter = e.key
keyMessage(e) {
if(e.keyCode >= 65 && e.keyCode <= 90) {
const letter = e.key
if(this.selectedWord.includes(letter)) {
if(!this.correctLetters.includes(letter)) {
this.correctLetters.push(letter)
if(this.selectedWord.includes(letter)) {
if(!this.correctLetters.includes(letter)) {
this.correctLetters.push(letter)
this.displayWord()
} else {
this.showNotification()
}
this.displayWord()
} else {
if(!this.wrongLetters.includes(letter)) {
this.wrongLetters.push(letter);
this.showNotification()
}
} else {
if(!this.wrongLetters.includes(letter)) {
this.wrongLetters.push(letter)
this.updateWrongLettersEl()
} else {
this.showNotification()
}
this.updateWrongLettersEl()
} else {
this.showNotification()
}
}
},
displayWord() {
this.wordEl =
this.selectedWord
}
},
displayWord() {
this.wordEl =
this.selectedWord
.split('')
.map(
letter => {
let toggleLetter = this.correctLetters.includes(letter) ? letter : '';
return '<span class="letter">' + toggleLetter +"</span>"
let toggleLetter = this.correctLetters.includes(letter) ? letter : ''
return '<span class="letter">' + toggleLetter + '</span>'
}
).join('');
).join('')
const innerWord = this.wordEl.replace(/<span class="letter">|<\/span>/g, '');
const innerWord = this.wordEl.replace(/<span class="letter">|<\/span>/g, '')
if(innerWord === this.selectedWord) {
this.finalMessage = 'Gagné! 😃';
this.popup = true
this.newGame()
}
},
updateWrongLettersEl() {
this.wrongLettersEl =
!this.wrongLetters ? '<p>Wrong</p>' : ''
+
this.wrongLetters.map(letter => `<span>${letter}</span>`);
// Display parts
this.figureParts.forEach((part, index) => {
const errors = this.wrongLetters.length;
if(index < errors) {
part.style.display = 'block'
} else {
part.style.display = 'none'
}
});
// Check if lost
if(this.wrongLetters.length === this.figureParts.length) {
this.finalMessage = 'Perdu. 😕';
this.popup = true
this.newGame()
}
},
newGame: function () {
if(innerWord === this.selectedWord) {
this.finalMessage = 'Gagné! 😃'
this.popup = true
this.correctLetters.splice(0);
this.wrongLetters.splice(0);
this.letterBox = ''
this.selectedWord = ''
this.wrongLettersEl = ''
},
showNotification(){
this.notification = true
setTimeout(() => {
this.notification = false
}, 2000)
this.newGame()
}
},
updateWrongLettersEl() {
this.wrongLettersEl =
!this.wrongLetters ? '<p>Wrong</p>' : ''
+
this.wrongLetters.map(letter => `<span>${letter}</span>`)
// Display parts
this.figureParts.forEach((part, index) => {
const errors = this.wrongLetters.length
if(index < errors) {
part.style.display = 'block'
} else {
part.style.display = 'none'
}
})
// Check if lost
if(this.wrongLetters.length === this.figureParts.length) {
this.finalMessage = 'Perdu. 😕'
this.popup = true
this.newGame()
}
},
newGame: function () {
this.popup = true
this.correctLetters.splice(0)
this.wrongLetters.splice(0)
this.letterBox = ''
this.selectedWord = ''
this.wrongLettersEl = ''
},
showNotification(){
this.notification = true
setTimeout(() => {
this.notification = false
}, 2000)
}
}
}
</script>

View File

@@ -6,13 +6,13 @@
</template>
<script>
export default {
name: "HangmanIndex",
props: {
link: {
type: String,
required: true
},
}
export default {
name: 'HangmanIndex',
props: {
link: {
type: String,
required: true
},
}
}
</script>

View File

@@ -6,12 +6,12 @@
</template>
<script>
import OpenWeatherCard from "./Meteo/OpenWeatherCard";
import OpenWeatherCard from './Meteo/OpenWeatherCard'
export default {
name: "Home",
components: {
OpenWeatherCard
}
export default {
name: 'Home',
components: {
OpenWeatherCard
}
}
</script>

View File

@@ -1,48 +1,45 @@
<template>
<div class="p-4">
<div class="flex justify-between flex-center mb-4">
<router-link to="/memos/" class="btn">< Back</router-link>
<button @click="$router.back()" class="btn-alert">Cancel</button>
<router-link to="/memos/" class="btn">Back</router-link>
<button class="btn-primary">Add New Memo</button>
</div>
<form @submit.prevent="submitForm">
<InputField name="name" label="Title" placeholder="Your Title" required @update:field="form.name = $event" :errors="errors" />
<TextAreaField class="" name="memo" placeholder="Your Memo" required @update:field="form.memo = $event" :errors="errors" />
<div class="flex justify-end mt-2">
<button class="btn-primary">Add New Memo</button>
</div>
</form>
</div>
</template>
<script>
import InputField from "../../components/InputField";
import TextAreaField from "../../components/TextAreaField";
import InputField from '../../components/InputField'
import TextAreaField from '../../components/TextAreaField'
export default {
name: "MemoCreate",
components: {
InputField, TextAreaField
},
data: function () {
return {
form: {
'name': '',
'memo': '',
},
errors: null,
}
},
methods: {
submitForm: function () {
axios.post('/api/memos', this.form)
.then(response => {
this.$router.push(response.data.links.self)
})
.catch(errors => {
this.errors = errors.response.data.errors
})
}
export default {
name: 'MemoCreate',
components: {
InputField, TextAreaField
},
data: function () {
return {
form: {
'name': '',
'memo': '',
},
errors: null,
}
},
methods: {
submitForm: function () {
// eslint-disable-next-line no-undef
axios.post('/api/memos', this.form)
.then(response => {
this.$router.push(response.data.links.self)
})
.catch(errors => {
this.errors = errors.response.data.errors
})
}
}
}
</script>

View File

@@ -14,16 +14,13 @@
:alt="form.name"/>
</div>
<div class="p-4">
<div class="flex justify-between mb-4">
<router-link :to="'/memos/' + this.$route.params.id" class="btn">< Back</router-link>
</div>
<form @submit.prevent="submitForm">
<div class="flex justify-between mb-4">
<router-link :to="'/memos/' + this.$route.params.id" class="btn">Back</router-link>
<button class="btn-primary">Save</button>
</div>
<InputField name="name" :data="form.name" label="Title" placeholder="Your Title" required @update:field="form.name = $event" :errors="errors" />
<TextAreaField class="memo-text-area" name="memo" :data="form.memo" placeholder="Your Memo" required @update:field="form.memo = $event" :errors="errors" />
<div class="flex justify-end mt-2">
<button class="btn-primary">Save</button>
</div>
</form>
</div>
@@ -31,49 +28,51 @@
</template>
<script>
import InputField from "../../components/InputField";
import TextAreaField from "../../components/TextAreaField";
import UploadableImage from "../../components/UploadableImage";
import InputField from '../../components/InputField'
import TextAreaField from '../../components/TextAreaField'
import UploadableImage from '../../components/UploadableImage'
export default {
name: "MemoEdit",
components: {
InputField, TextAreaField, UploadableImage
},
data: function () {
return {
form: {
'name': '',
'memo': '',
'attributes': {}
},
errors: null,
loading: true,
}
},
methods: {
submitForm: function () {
axios.patch('/api/memos/' + this.$route.params.id, this.form)
.then(response => {
this.$router.push(response.data.links.self)
})
.catch(errors => {
this.errors = errors.response.data.errors
})
}
},
mounted() {
axios.get('/api/memos/' + this.$route.params.id)
export default {
name: 'MemoEdit',
components: {
InputField, TextAreaField, UploadableImage
},
data: function () {
return {
form: {
'name': '',
'memo': '',
'attributes': {}
},
errors: null,
loading: true,
}
},
methods: {
submitForm: function () {
// eslint-disable-next-line no-undef
axios.patch('/api/memos/' + this.$route.params.id, this.form)
.then(response => {
this.form = response.data.data
this.loading = false
this.$router.push(response.data.links.self)
})
.catch(error => {
this.loading = false
if (error.response.status === 404) {
this.$router.back()
}
.catch(errors => {
this.errors = errors.response.data.errors
})
}
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/memos/' + this.$route.params.id)
.then(response => {
this.form = response.data.data
this.loading = false
})
.catch(error => {
this.loading = false
if (error.response.status === 404) {
this.$router.back()
}
})
}
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="p-4">
<div class="flex justify-between flex-center mb-4">
<a href="#" class="btn" @click="$router.back()">< Back</a>
<a href="#" class="btn" @click="$router.back()">Back</a>
<router-link :to="'/memos/create'" class="btn-primary">Add New Memo</router-link>
</div>
<Loader v-if="loading" />
@@ -25,29 +25,30 @@
</template>
<script>
import Loader from "../../components/Loader";
import Loader from '../../components/Loader'
export default {
name: "MemoIndex",
components: {
Loader
},
data: function () {
return {
loading: true,
memos: null,
}
},
mounted() {
axios.get('/api/memos')
.then(response => {
this.memos = response.data.data
this.loading = false
})
.catch(error => {
this.loading = false
console.log('Unable to fetch memos.')
})
export default {
name: 'MemoIndex',
components: {
Loader
},
data: function () {
return {
loading: true,
memos: null,
}
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/memos')
.then(response => {
this.memos = response.data.data
this.loading = false
})
.catch(() => {
this.loading = false
console.log('Unable to fetch memos.')
})
}
}
</script>

View File

@@ -18,7 +18,7 @@
/>
<div class="absolute flex flex-col justify-between w-full top-0 bottom-0">
<div class="flex items-center justify-between p-4">
<router-link to="/memos/" class="btn-secondary">< Back</router-link>
<router-link to="/memos/" class="btn-secondary">Back</router-link>
<div class="flex-middle">
<router-link :to="'/memos/' + memo.memo_id + '/edit'" class="btn-secondary mr-1" >Edit</router-link>
<a href="#" class="btn-alert" @click="modal = ! modal">Delete</a>
@@ -30,61 +30,63 @@
<!-- <TagBox :memo="memo" />-->
<div class="p-4">
<p class="pt-2" v-html="memoMarkdown"></p>
<div class="bg-orange-400 rounded mt-2 px-2 py-1 flex justify-end">@last update {{ memo.last_updated }}</div>
<div class="memo-style p-4 pb-3 -mb-1 bg-white" v-html="memoMarkdown"></div>
<div class="bg-orange-400 px-2 py-1 flex justify-end">@last update {{ memo.last_updated }}</div>
</div>
</div>
</div>
</template>
<script>
// import TagBox from "../Tag/TagBox";
import Loader from "../../components/Loader";
let MarkdownIt = require('markdown-it'),
md = new MarkdownIt()
.use(require('markdown-it-checkbox'));
// import TagBox from "../Tag/TagBox";
import Loader from '../../components/Loader'
let MarkdownIt = require('markdown-it'),
md = new MarkdownIt()
.use(require('markdown-it-checkbox'))
export default {
name: 'MemoShow',
components: {
Loader
// TagBox
},
data: function () {
return {
loading: true,
modal: false,
memo: null,
}
},
computed: {
memoMarkdown: function () {
return md.render(this.memo.memo)
}
},
mounted() {
axios.get('/api/memos/' + this.$route.params.id)
.then(response => {
this.memo = response.data.data
this.loading = false
export default {
name: 'MemoShow',
components: {
Loader
// TagBox
},
data: function () {
return {
loading: true,
modal: false,
memo: null,
}
},
computed: {
memoMarkdown: function () {
return md.render(this.memo.memo)
}
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/memos/' + this.$route.params.id)
.then(response => {
this.memo = response.data.data
this.loading = false
})
.catch(errorRes => {
this.loading = false
if (errorRes.response.status === 404) {
this.$router.push('/memos')
}
})
},
methods: {
destroy: function () {
// eslint-disable-next-line no-undef
axios.delete('/api/memos/' + this.$route.params.id)
.then(() => {
this.$router.push('/memos')
})
.catch(errorRes => {
this.loading = false
if (errorRes.response.status === 404) {
this.$router.push('/memos')
}
console.log('Internal Error, Unable to delete contact.' + errorRes)
})
},
methods: {
destroy: function () {
axios.delete('/api/memos/' + this.$route.params.id)
.then(response => {
this.$router.push('/memos')
})
.catch(errorRes => {
console.log('Internal Error, Unable to delete contact.' + errorRes)
})
}
}
}
}
</script>

View File

@@ -22,50 +22,50 @@
</template>
<script>
export default {
name: "OpenWeatherCard",
data: function () {
return {
loading: true,
meteo: null,
}
},
methods : {
dateFormat(date) {
const d = new Date(date)
const dtf = new Intl.DateTimeFormat('fr', { year: '2-digit', month: 'short', day: '2-digit', hour: 'numeric' })
const [{ value: mo },,{ value: da },,{ value: ye },,{value: ho }] = dtf.formatToParts(d)
return `${mo}/${da} - ${ho}h`
}
},
mounted() {
let refreshMeteo = false
export default {
name: 'OpenWeatherCard',
data: function () {
return {
loading: true,
meteo: null,
}
},
methods : {
dateFormat(date) {
const d = new Date(date)
const dtf = new Intl.DateTimeFormat('fr', { year: '2-digit', month: 'short', day: '2-digit', hour: 'numeric' })
const [{ value: mo },,{ value: da },,{value: ho }] = dtf.formatToParts(d)
return `${mo}/${da} - ${ho}h`
}
},
mounted() {
let refreshMeteo = false
if(localStorage.getItem('meteo')) {
this.meteo = JSON.parse(localStorage.getItem('meteo'))
let deltaTime = new Date() - new Date(this.meteo.list[0].dt_txt)
if (deltaTime/1000/3600 > 3) {
refreshMeteo = true
}
this.loading = false
console.log('in storage', this.meteo.list[0], this.meteo.list[0].dt_txt)
} else {
if(localStorage.getItem('meteo')) {
this.meteo = JSON.parse(localStorage.getItem('meteo'))
let deltaTime = new Date() - new Date(this.meteo.list[0].dt_txt)
if (deltaTime/1000/3600 > 3) {
refreshMeteo = true
}
this.loading = false
console.log('in storage', this.meteo.list[0], this.meteo.list[0].dt_txt)
} else {
refreshMeteo = true
}
if (refreshMeteo) {
console.log("refreshing meteo data")
axios.get('/api/meteo')
.then(response => {
this.meteo = response.data
this.loading = false
localStorage.setItem('meteo', JSON.stringify(response.data));
})
.catch(error => {
console.log('Unable to fetch meteo.')
})
}
},
}
if (refreshMeteo) {
// eslint-disable-next-line no-undef
axios.get('/api/meteo')
.then(response => {
this.meteo = response.data
this.loading = false
localStorage.setItem('meteo', JSON.stringify(response.data))
})
.catch(() => {
console.log('Unable to fetch meteo.')
})
}
},
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<li class="todo flex justify-between items-center bg-white rounded mb-1 px-2 py-1"
>
<div class="flex flex-1">
<svg-vue icon="draggable" class="w-4 block mr-2 cursor-move" />
{{ toDo.data.attributes.data.name }}
</div>
<div class="flex">
<svg-vue icon="edit" @click="edit = !edit" class="edit-icon z-10 w-4 block cursor-pointer mr-1" />
<input type="checkbox" v-model="checked">
<span v-if="edit" @click="deleteToDo(toDo, position)">X</span>
</div>
</li>
</template>
<script>
export default {
name: 'ToDo',
data: function () {
return {
name: this.toDo.data.attributes.data.name,
errors: null,
edit: false,
checked: false,
}
},
props: {
toDo: {
type: Object,
require: true
},
position: {
type: Number,
require: true
}
},
mounted() {
console.log(this.$key)
}
}
</script>
<style scoped>
.edit-icon {
opacity: 0;
transition: opacity 0.2s;
}
.todo:hover .edit-icon {
opacity: 1;
transition: opacity 0.2s;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div>
<div class="m-2 px-2 pt-2 bg-orange-400 rounded flex flex-col justify-between shadow">
<div>
<h1 class="text-2xl font-bold mb-2">{{ toDoList.data.attributes.data.name }}</h1>
<ul class="draggable-list drop-zone"
@drop='onDrop($event, toDoList)'
@dragover.prevent
@dragenter.prevent
>
<div v-if="toDoList.data.attributes.data.to_dos.to_dos_count < 1">
------- no to Do -------
</div>
<ToDo v-else
v-for="(toDo, indexToDo) in toDoList.data.attributes.data.to_dos.data"
:key="indexToDo"
:toDo="toDo"
:position="indexToDo"
draggable
@dragstart='startDrag($event, toDo)'
class='drag-el'
/>
</ul>
</div>
<div class="flex items-center mt-2">
<InputField name="name" classes="py-1" placeholder="New To Do" required @update:field="name = $event" :errors="errors" />
<button class="btn-primary ml-1 mb-2 py-1" @click="addToDo">ADD</button>
</div>
</div>
</div>
</template>
<script>
import InputField from '../../components/InputField'
import ToDo from './ToDo'
//https://learnvue.co/2020/01/how-to-add-drag-and-drop-to-your-vuejs-project/
export default {
name: 'ToDoList',
components: {
InputField, ToDo
},
data: function () {
return {
name: '',
errors: null,
dragStartIndex: null,
edit: false,
}
},
props: {
toDoList: {
type: Object,
require: true
}
},
methods: {
addToDo: function () {
// eslint-disable-next-line no-undef
axios.post('/api/to-do-lists/' + this.toDoList.data.to_do_list_id + '/to-do', {name: this.name})
.then(res => {
this.toDoList.data.attributes.data.to_dos.data.push(res.data)
this.name = ''
})
.catch(errorRes => {
console.log('Internal Error, Unable to delete contact.' + errorRes)
})
},
deleteToDo: function (toDo, position) {
// eslint-disable-next-line no-undef
axios.delete('/api/to-do-lists/' + this.toDoList.data.to_do_list_id + '/to-do/' + toDo.data.to_do_id)
.then(() => {
this.toDoList.data.attributes.data.to_dos.data.splice(position, 1)
})
.catch(errorRes => {
console.log('Internal Error, Unable to delete contact.' + errorRes)
})
},
startDrag: (evt, item) => {
console.log('StartDrag', evt, item)
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('itemID', item.id)
},
onDrop (evt, list) {
console.log('onDrop', evt, list, this.toDoList)
const itemID = evt.dataTransfer.getData('itemID')
const item = this.toDoList.data.attributes.data.to_dos.data.find(item => item.id == itemID)
item.list = list
},
}
}
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="p-2">
<div v-if="modal" class="modal-container" @click="modal = ! modal"></div>
<div v-if="modal" class="modal px-2">
<p class="m-1 text-center">Add a new to-do list ?</p>
<InputField name="name" label="Title" placeholder="Your Title" required @update:field="name = $event" :errors="errors" />
<div class="flex-center m-1 mt-2">
<button class="btn-secondary mr-2" @click="modal = ! modal">Cancel</button>
<button class="btn-primary" @click="create">Create</button>
</div>
</div>
<div class="flex-between flex-center mb-1">
<a href="#" class="btn" @click="$router.back()">Back</a>
<a href="#" class="btn-primary" @click="modal = ! modal">Add New List</a>
</div>
<Loader v-if="loading" />
<div v-else class="flex flex-wrap -m-2 mt-2">
<div v-if="toDoLists.length < 1">No List Yet</div>
<ToDoList v-else
v-for="(toDoList, index) in toDoLists"
:key="index"
:to-do-list="toDoList"
class="w-full sm:w-1/2 md:w-1/3 lg:w-1/4" />
</div>
</div>
</template>
<script>
import Loader from '../../components/Loader'
import InputField from '../../components/InputField'
import ToDoList from './ToDoList'
export default {
name: 'ToDoListIndex',
components: {
Loader, InputField, ToDoList
},
data: function () {
return {
loading: true,
modal: false,
toDoLists: null,
name: '',
errors: null,
}
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/to-do-lists')
.then(res => {
this.toDoLists = res.data.data
this.loading = false
})
.catch(errorRes => {
this.loading = false
if (errorRes.response.status === 404) {
this.$router.push('/')
}
})
},
methods: {
create: function () {
// eslint-disable-next-line no-undef
axios.post('/api/to-do-lists', {name: this.name})
.then(res => {
console.log(res)
this.modal = false
this.name = ''
this.toDoLists.push(res.data)
})
.catch(errorRes => {
console.log('Internal Error, Unable to delete contact.' + errorRes)
})
}
}
}
</script>

View File

@@ -0,0 +1,9 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'ToDoListShow'
}
</script>

View File

@@ -29,18 +29,18 @@
</template>
<script>
import { mapGetters } from 'vuex'
import UploadableImage from "../../components/UploadableImage";
import { mapGetters } from 'vuex'
import UploadableImage from '../../components/UploadableImage'
export default {
name: 'Profil',
components: {
UploadableImage
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
}
export default {
name: 'Profil',
components: {
UploadableImage
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
}
</script>

View File

@@ -11,31 +11,28 @@
</template>
<script>
import UploadableImage from "../../components/UploadableImage";
export default {
name: 'Profile',
components: {
UploadableImage
},
data: function () {
return {
loading: true,
user: null,
}
},
mounted() {
axios.get('/api/users/' + this.$route.params.id)
.then(response => {
this.user = response.data.data
this.loading = false
})
.catch(errorRes => {
this.loading = false
if (errorRes.response.status === 404) {
this.$router.push('/user')
}
})
},
}
export default {
name: 'Profile',
data: function () {
return {
loading: true,
user: null,
}
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/users/' + this.$route.params.id) // eslint-disable-line no-undef-init
.then(response => {
this.user = response.data.data
this.loading = false
})
.catch(errorRes => {
this.loading = false
if (errorRes.response.status === 404) {
this.$router.push('/user')
}
})
},
}
</script>

View File

@@ -1,108 +1,110 @@
<template>
<div v-if="authUser">
<div class="flex m-4">
<div class="avatar mr-2">
<Avatar :avatar="authUser.data.attributes.avatar" size="6xl" :alt="authUser.data.attributes.name" class="w-24 h-24" />
</div>
<div class="flex flex-col justify-center ml-2">
<div><strong>{{ authUser.data.attributes.name }}</strong></div>
<div><strong>{{ authUser.data.attributes.email }}</strong></div>
</div>
</div>
<div v-if="authUser.data.attributes.is_admin">
<div class="box-toggle">
<div class="box-toggle-header" @click="userAddToggle = !userAddToggle">
<h2 class="mb-1">Ajouter un membre</h2>
<svg-vue icon="arrow" v-bind:class="{ open: userAddToggle }" />
</div>
<transition name="fade">
<form @submit.prevent="addMember" v-if="userAddToggle" class="box-toggle-content" >
<AlertBox v-if="alertType" :type="alertType" :message="alertMessage" class="mb-1" />
<div class="flex mb-2">
<InputField name="name" label="Nom du nouveau membre" placeholder="Nom" required :errors="errors" @update:field="form.name = $event" class="mr-1" />
<InputField name="email" type="email" label="Adresse email du nouveau membre" placeholder="E-mail" required :errors="errors" @update:field="form.email = $event" class="mr-1" />
<div class="flex items-end mb-2">
<button class="btn-primary">Ajouter</button>
</div>
</div>
</form>
</transition>
</div>
<div class="mb-2 box-toggle">
<div class="box-toggle-header" @click="userListToggle = !userListToggle">
<h2>Liste des utilisateurs</h2>
<svg-vue icon="arrow" v-bind:class="{ open: userListToggle }" />
<div v-if="authUser">
<div class="flex m-4">
<div class="avatar mr-2">
<Avatar :avatar="authUser.data.attributes.avatar" size="6xl" :alt="authUser.data.attributes.name" class="w-24 h-24" />
</div>
<div class="flex flex-col justify-center ml-2">
<div><strong>{{ authUser.data.attributes.name }}</strong></div>
<div><strong>{{ authUser.data.attributes.email }}</strong></div>
</div>
<transition name="fade">
<ul v-if="userListToggle" class="box-toggle-content">
<Loader v-if="loading" />
<li v-else v-for="user in users">
<router-link :to="'/users/' + user.data.user_id">{{ user.data.attributes.name }}</router-link> - {{ user.data.attributes.email }} - {{ user.data.attributes.last_login }} | {{ user.data.attributes.is_admin }}
</li>
</ul>
</transition>
</div>
<div>Css Testeur pour constituer un thème : <router-link to="/css-testeur">Css Testeur</router-link></div>
<div v-if="authUser.data.attributes.is_admin">
<div class="box-toggle">
<div class="box-toggle-header" @click="userAddToggle = !userAddToggle">
<h2 class="mb-1">Ajouter un membre</h2>
<svg-vue icon="arrow" v-bind:class="{ open: userAddToggle }" />
</div>
<transition name="fade">
<form @submit.prevent="addMember" v-if="userAddToggle" class="box-toggle-content" >
<AlertBox v-if="alertType" :type="alertType" :message="alertMessage" class="mb-1" />
<div class="flex mb-2">
<InputField name="name" label="Nom du nouveau membre" placeholder="Nom" required :errors="errors" @update:field="form.name = $event" class="mr-1" />
<InputField name="email" type="email" label="Adresse email du nouveau membre" placeholder="E-mail" required :errors="errors" @update:field="form.email = $event" class="mr-1" />
<div class="flex items-end mb-2">
<button class="btn-primary">Ajouter</button>
</div>
</div>
</form>
</transition>
</div>
<div class="mb-2 box-toggle">
<div class="box-toggle-header" @click="userListToggle = !userListToggle">
<h2>Liste des utilisateurs</h2>
<svg-vue icon="arrow" v-bind:class="{ open: userListToggle }" />
</div>
<transition name="fade">
<ul v-if="userListToggle" class="box-toggle-content">
<Loader v-if="loading" />
<li v-else v-for="(user, index) in users" :key="index">
<router-link :to="'/users/' + user.data.user_id">{{ user.data.attributes.name }}</router-link> - {{ user.data.attributes.email }} - {{ user.data.attributes.last_login }} | {{ user.data.attributes.is_admin }}
</li>
</ul>
</transition>
</div>
<div>Css Testeur pour constituer un thème : <router-link to="/css-testeur">Css Testeur</router-link></div>
</div>
</div>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import Avatar from '../../components/Avatar';
import AlertBox from '../../components/AlertBox';
import InputField from '../../components/InputField';
import Loader from "../../components/Loader";
import {mapGetters} from 'vuex'
import Avatar from '../../components/Avatar'
import AlertBox from '../../components/AlertBox'
import InputField from '../../components/InputField'
import Loader from '../../components/Loader'
export default {
name: 'UserAdmin',
components: {
Avatar, AlertBox, InputField, Loader
},
data: function () {
return {
form: {
name: '',
email: '',
},
alertType: '',
alertMessage: '',
errors: null,
loading: true,
users: null,
userListToggle: false,
userAddToggle: false,
}
},
computed: {
...mapGetters({
authUser: 'authUser',
export default {
name: 'UserAdmin',
components: {
Avatar, AlertBox, InputField, Loader
},
data: function () {
return {
form: {
name: '',
email: '',
},
alertType: '',
alertMessage: '',
errors: null,
loading: true,
users: null,
userListToggle: false,
userAddToggle: false,
}
},
computed: {
...mapGetters({
authUser: 'authUser',
})
},
mounted() {
// eslint-disable-next-line no-undef
axios.get('/api/users')
.then(response => {
this.users = response.data.data
this.loading = false
})
},
mounted() {
axios.get('/api/users')
.then(response => {
this.users = response.data.data
this.loading = false
.catch(error => {
this.loading = false
console.log('Unable to fetch users.' + error)
})
},
methods: {
addMember: function () {
// eslint-disable-next-line no-undef
axios.post('/api/users', {name: this.form.name, email: this.form.email})
.then(res => {
this.form.name = ''
this.form.email = ''
this.alertType = 'success'
this.alertMessage = `${res.data.data.attributes.name} a bien été créé`
})
.catch(error => {
this.loading = false
console.log('Unable to fetch users.')
.catch(errors => {
this.errors = errors.response.data.errors
})
},
methods: {
addMember: function () {
axios.post('/api/users', {name: this.form.name, email: this.form.email})
.then(res => {
this.form.name = ''
this.form.email = ''
this.alertType = 'success'
this.alertMessage = `${res.data.data.attributes.name} a bien été créé`
})
.catch(errors => {
this.errors = errors.response.data.errors
})
}
}
}
}
</script>

View File

@@ -26,7 +26,7 @@
//
//@import "pages/auth";
//@import "pages/users";
//@import "pages/memos";
@import "pages/memos";
//@import "pages/meteo";
//@import "pages/games";
//

View File

@@ -129,7 +129,7 @@ nav {
span {
@apply font-bold text-xl ml-2 overflow-hidden;
@apply font-bold text-xl ml-2 overflow-hidden truncate;
transition: width 0.3s;
&:hover {

View File

@@ -1,3 +1,4 @@
/*
.memo {
&-list {
@@ -81,3 +82,57 @@
width: 100%;
}
}
*/
.memo-style {
h1,
h2,
h3,
h4 {
@apply font-bold;
}
h1 {
@apply text-4xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
p {
@apply mb-2 mt-1;
text-indent: 2rem;
}
pre,
code {
@apply bg-gray-700 text-white p-1;
}
pre {
@apply my-2;
}
blockquote {
@apply bg-gray-300 italic p-2 mx-4 my-2;
}
ul {
@apply list-disc ml-8;
}
ol {
@apply list-decimal ml-8;
}
}

View File

@@ -0,0 +1,7 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 384 384" style="enable-background:new 0 0 384 384;" xml:space="preserve">
<g>
<rect x="0" y="106.667" width="384" height="42.667"/>
<rect x="0" y="234.667" width="384" height="42.667"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 340 B

6
resources/svg/edit.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 383.947 383.947" style="enable-background:new 0 0 383.947 383.947;" xml:space="preserve">
<g>
<polygon points="0,303.947 0,383.947 80,383.947 316.053,147.893 236.053,67.893"/>
<path d="M377.707,56.053L327.893,6.24c-8.32-8.32-21.867-8.32-30.187,0l-39.04,39.04l80,80l39.04-39.04C386.027,77.92,386.027,64.373,377.707,56.053z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

11
resources/svg/list.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 384 384" style="enable-background:new 0 0 384 384;" xml:space="preserve">
<g>
<rect x="0" y="170.667" width="42.667" height="42.667"/>
<rect x="0" y="85.333" width="42.667" height="42.667"/>
<rect x="0" y="256" width="42.667" height="42.667"/>
<rect x="85.333" y="85.333" width="298.667" height="42.667"/>
<rect x="85.333" y="170.667" width="298.667" height="42.667"/>
<rect x="85.333" y="256" width="298.667" height="42.667"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@@ -1,51 +1,52 @@
@extends('layouts.app')
@section('content')
<div class="auth p-2">
<div class="title-page mb-2">{{ __('Login') }}</div>
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="bg-orange-200 mx-auto h-screen flex justify-center items-center">
<div class="w-96 m-auto bg-orange-400 rounded p-4 mt-10 shadow-xl">
<div class="text-3xl font-bold text-center mb-4">{{ __('Login') }}</div>
<form method="POST" action="{{ route('login') }}">
@csrf
<label for="email" class="mb-1">{{ __('E-Mail') }}</label>
<div class="mb-2">
<input id="email" type="email" class="@error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<label for="email" class="mb-2 font-bold uppercase">{{ __('E-Mail') }}</label>
<div class="mb-4">
<input id="email" type="email" class="w-full border-2 border-orange-500 focus:border-orange-700 rounded p-2 @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<label for="password" class="mb-1">{{ __('Password') }}</label>
<div class="mb-2">
<input id="password" type="password" class="@error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
<label for="password" class="mb-2 font-bold uppercase">{{ __('Password') }}</label>
<div class="mb-4">
<input id="password" type="password" class="w-full border-2 border-orange-500 focus:border-orange-700 rounded p-2 @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
<div class="mb-2">
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<div class="mb-4">
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label for="remember" class="inline">
{{ __('Remember Me') }}
</label>
</div>
<div class="flex-between">
<button type="submit" class="btn-primary px-3">
{{ __('Login') }}
</button>
@if (Route::has('password.request'))
<a class="btn-secondary" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</form>
<label for="remember" class="inline">
{{ __('Remember Me') }}
</label>
</div>
<div class="flex justify-between">
<button type="submit" class="btn-primary">
{{ __('Login') }}
</button>
@if (Route::has('password.request'))
<a class="btn-secondary" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</form>
</div>
</div>
@endsection

View File

@@ -31,5 +31,6 @@ Route::middleware('auth:api')->group(function () {
Route::post('/images/users/{users}', 'ImageController@users');
Route::post('/images/memos/{memo}', 'ImageController@memos');
Route::patch('/to-do-lists/{toDoList}/to-do/{toDo}/change', 'ToDoController@changeOrder');
});

View File

@@ -34,7 +34,7 @@ class ToDoItemsTest extends TestCase
'attributes' => [
'data' => [
'name' => $toDo->name,
'order' => 0,
'order' => 1,
]
],
],
@@ -78,6 +78,11 @@ class ToDoItemsTest extends TestCase
[
'data' => [
'to_do_id' => $toDoList->toDos[0]->id,
'attributes' => [
'data' => [
'name' => $toDoList->toDos[0]->name,
]
]
]
]
],
@@ -89,6 +94,87 @@ class ToDoItemsTest extends TestCase
]);
}
/** @test */
public function a_new_to_do_must_have_order_of_the_previous_to_do()
{
$this->actingAs($user = factory(User::class)->create(), 'api');
$toDoList = factory(ToDoList::class)->create(['id' => 123, 'user_id' => $user->id]);
$toDo = factory(ToDo::class)->create(['to_do_list_id' => 123]);
$response = $this->post('/api/to-do-lists/123/to-do', [
'name' => 'New To Do'
])->assertStatus(201);
$toDo = ToDo::first();
$newToDo = ToDo::orderBy('id', 'desc')->first();
$this->assertEquals('New To Do', $newToDo->name);
$this->assertEquals($toDo->order + 1, $newToDo->order);
}
/** @test */
public function a_second_to_do_as_next_order_of_list()
{
$this->actingAs($user = factory(User::class)->create(), 'api');
$toDoList = factory(ToDoList::class)->create(['id' => 123, 'user_id' => $user->id]);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test name to do']);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 2 name to do']);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 3 name to do']);
$response = $this->get('/api/to-do-lists/' . $toDoList->id );
$response->assertJson([
'data' => [
'to_do_list_id' => $toDoList->id,
'attributes' => [
'data' => [
'name' => $toDoList->name,
'last_updated' => $toDoList->updated_at->diffForHumans(),
'to_dos' => [
'data' => [
[
'data' => [
'to_do_id' => $toDoList->toDos[0]->id,
'attributes' => [
'data' => [
'name' => $toDoList->toDos[0]->name,
'order' => 1,
]
]
],
],
[
'data' => [
'to_do_id' => $toDoList->toDos[1]->id,
'attributes' => [
'data' => [
'name' => $toDoList->toDos[1]->name,
'order' => 2,
]
]
]
],
[
'data' => [
'to_do_id' => $toDoList->toDos[2]->id,
'attributes' => [
'data' => [
'name' => $toDoList->toDos[2]->name,
'order' => 3,
]
]
]
]
],
'to_dos_count' => 3,
]
]
],
]
]);
}
/** @test */
public function a_to_do_can_be_patch()
{
@@ -101,7 +187,6 @@ class ToDoItemsTest extends TestCase
]);
$toDoList = $toDoList->fresh();
// $toDoList = ToDoList::first();
$response = $this->patch('/api/to-do-lists/123/to-do/'. $toDoList->toDos[0]->id, [
'name' => 'To Do update'
@@ -121,4 +206,77 @@ class ToDoItemsTest extends TestCase
]
]);
}
/** @test */
public function only_the_owner_can_patch_the_to_do()
{
$user = factory(User::class)->create();
$toDoList = factory(ToDoList::class)->create(['id' => 123, 'user_id' => $user->id]);
$toDo = factory(ToDo::class)->create(['to_do_list_id' => 123, 'name' => 'Test name to do']);
$this->actingAs($anotherUser = factory(User::class)->create(), 'api');
$this->patch('/api/to-do-lists/'. $toDoList->id .'/to-do/'. $toDoList->toDos[0]->id, ['name' => 'Name changed'])
->assertStatus(403);
}
/** @test */
public function a_to_do_can_changer_of_order()
{
$this->withoutExceptionHandling();
$this->actingAs($user = factory(User::class)->create(), 'api');
$toDoList = factory(ToDoList::class)->create(['id' => 123, 'user_id' => $user->id]);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test name to do']);
$toDoReplaced = $this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 2 name to do']);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 3 name to do']);
$this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 4 name to do']);
$todoMove = $this->post('/api/to-do-lists/123/to-do', ['name' => 'Test 5 name to do']);
$response = $this->patch('/api/to-do-lists/'. $toDoList->id .'/to-do/'. $todoMove['data']['to_do_id'] .'/change', ['new-order' => $toDoReplaced['data']['attributes']['data']['order']])
->assertStatus(200)
->assertJson([
'data' => [
'to_do_id' => $todoMove['data']['to_do_id'],
'attributes' => [
'data' => [
'order' => $toDoReplaced['data']['attributes']['data']['order'],
]
]
],
'links' => [
'self' => url('/to-do-lists/'. $toDoList->id),
]
]);
}
/** @test */
public function a_to_do_list_can_be_delete()
{
$this->withoutExceptionHandling();
$this->actingAs($user = factory(User::class)->create(), 'api');
$toDoList = factory(ToDoList::class)->create(['user_id' => $user->id]);
$toDo = factory(ToDo::class)->create(['to_do_list_id' => $toDoList->id]);
$response = $this->delete('/api/to-do-lists/'. $toDoList->id . '/to-do/' . $toDo->id);
$this->assertCount(0, ToDo::all());
$response->assertStatus(204);
}
/** @test */
public function only_the_owner_can_delete_the_to_do_list()
{
$user = factory(User::class)->create();
$toDoList = factory(ToDoList::class)->create(['user_id' => $user->id]);
$toDo = factory(ToDo::class)->create(['to_do_list_id' => $toDoList->id]);
$this->actingAs($anotherUser = factory(User::class)->create(), 'api');
$response = $this->delete('/api/to-do-lists/'. $toDoList->id . '/to-do/' . $toDo->id);
$response->assertStatus(403);
}
}

View File

@@ -88,6 +88,50 @@ class ToDoListsTest extends TestCase
$response->assertStatus(403);
}
/** @test */
public function a_user_can_retrueved_all_this_to_do_lists()
{
$this->actingAs($user = factory(User::class)->create(), 'api');
$toDoListOne = factory(ToDoList::class)->create(['user_id' => $user->id]);
$toDoListTwo = factory(ToDoList::class)->create(['user_id' => $user->id]);
$response = $this->get('/api/to-do-lists');
$response->assertJson([
'data' => [
[
'data' => [
'to_do_list_id' => $toDoListOne->id,
'attributes' => [
'data' => [
'name' => $toDoListOne->name,
'last_updated' => $toDoListOne->updated_at->diffForHumans(),
]
],
],
'links' => [
'self' => $toDoListOne->path(),
]
],
[
'data' => [
'to_do_list_id' => $toDoListTwo->id,
'attributes' => [
'data' => [
'name' => $toDoListTwo->name,
'last_updated' => $toDoListTwo->updated_at->diffForHumans(),
]
],
],
'links' => [
'self' => $toDoListTwo->path(),
]
]
]
]);
}
/** @test */
public function a_to_do_list_can_be_patch()
{