Compare commits
10 Commits
00820715ea
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c054f791b1 | ||
|
|
00bb86f798 | ||
|
|
4536195a4c | ||
|
|
5350c5c46b | ||
|
|
8dbd63478d | ||
|
|
5c154b740c | ||
|
|
040ca01158 | ||
|
|
140d68c3a9 | ||
|
|
a0f06d3bbe | ||
|
|
67e970183f |
@@ -1,42 +0,0 @@
|
|||||||
// filedropzone_controller.js
|
|
||||||
|
|
||||||
import { Controller } from '@hotwired/stimulus';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
connect() {
|
|
||||||
this.element.addEventListener('dropzone:connect', this._onConnect);
|
|
||||||
this.element.addEventListener('dropzone:change', this._onChange);
|
|
||||||
this.element.addEventListener('dropzone:clear', this._onClear);
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
// You should always remove listeners when the controller is disconnected to avoid side-effects
|
|
||||||
this.element.removeEventListener('dropzone:connect', this._onConnect);
|
|
||||||
this.element.removeEventListener('dropzone:change', this._onChange);
|
|
||||||
this.element.removeEventListener('dropzone:clear', this._onClear);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onConnect(event) {
|
|
||||||
// The dropzone was just created
|
|
||||||
console.info('onconnect');
|
|
||||||
}
|
|
||||||
|
|
||||||
_onChange(event) {
|
|
||||||
// The dropzone just changed
|
|
||||||
console.info('onchange');
|
|
||||||
axios.post(document.URL, new FormData(event.target.closest('form')), {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
event.target.querySelector('.dropzone-preview-button').click();
|
|
||||||
window.EventBus.$emit('fileUploaded');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_onClear(event) {
|
|
||||||
// The dropzone has just been cleared
|
|
||||||
console.info('onclear');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
assets/js/api/book.js
Normal file
21
assets/js/api/book.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
booksFetch(searchTitle) {
|
||||||
|
return axios.get('/api/books', {
|
||||||
|
params: {title: searchTitle},
|
||||||
|
headers: {'accept': 'application/json'}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
bookUpdateProgress(bookId, progress) {
|
||||||
|
return axios.post('/progress/update', {
|
||||||
|
bookId: bookId,
|
||||||
|
progress: progress
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'content-type': 'text/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
20
assets/js/api/file.js
Normal file
20
assets/js/api/file.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
deleteFile: function (fileId) {
|
||||||
|
return axios.get(window.location.origin + '/file/delete/' + fileId);
|
||||||
|
},
|
||||||
|
getFiles: function (bookId) {
|
||||||
|
return axios.get(this.getFilesEndpoint(bookId), {
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getFilesEndpoint: function (bookId) {
|
||||||
|
if (bookId) {
|
||||||
|
return `/api/books/${bookId}/files`;
|
||||||
|
}
|
||||||
|
return `/api/files`;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
assets/js/api/user.js
Normal file
13
assets/js/api/user.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
login: function (email, password) {
|
||||||
|
return axios.post('/login_api', {
|
||||||
|
email: email,
|
||||||
|
password: password
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getUserData: function (userIri) {
|
||||||
|
return axios.get(userIri);
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Book from './pages/book'
|
import Book from './pages/book'
|
||||||
|
import BookListing from './pages/booklisting'
|
||||||
|
import BookListingHeader from './pages/booklistingheader'
|
||||||
|
import store from "./store/index";
|
||||||
|
|
||||||
Vue.component('Book', Book);
|
Vue.component('Book', Book);
|
||||||
|
Vue.component('BookListing', BookListing);
|
||||||
|
Vue.component('BookListingHeader', BookListingHeader);
|
||||||
|
|
||||||
new Vue().$mount('#app');
|
new Vue({store}).$mount('#app');
|
||||||
47
assets/js/components/loginform.vue
Normal file
47
assets/js/components/loginform.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<form v-on:submit.prevent="handleSubmit">
|
||||||
|
<div v-if="error" class="alert alert-danger">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exampleInputEmail1">Email address</label>
|
||||||
|
<input type="email" v-model="email" class="form-control" id="exampleInputEmail1"
|
||||||
|
aria-describedby="emailHelp" placeholder="Enter email">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="exampleInputPassword1">Password</label>
|
||||||
|
<input type="password" v-model="password" class="form-control"
|
||||||
|
id="exampleInputPassword1" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="exampleCheck1">
|
||||||
|
<label class="form-check-label" for="exampleCheck1">I like cheese</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" v-bind:class="{ disabled: isLoading }">Log in</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
error: '',
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: ['user'],
|
||||||
|
methods: {
|
||||||
|
...mapActions('usermodule', ['login']),
|
||||||
|
handleSubmit() {
|
||||||
|
this.login({'email': this.email, 'password': this.password});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
</style>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
window.EventBus = new Vue();
|
|
||||||
export const EventBus = window.EventBus;
|
|
||||||
@@ -2,8 +2,9 @@ import Vue from 'vue';
|
|||||||
import Files from "./pages/files";
|
import Files from "./pages/files";
|
||||||
import Progressbar from "./pages/progressbar";
|
import Progressbar from "./pages/progressbar";
|
||||||
import Progresseditor from "./pages/progresseditor";
|
import Progresseditor from "./pages/progresseditor";
|
||||||
|
import store from "./store/index";
|
||||||
|
|
||||||
Vue.component('Files', Files);
|
Vue.component('Files', Files);
|
||||||
Vue.component('Progressbar', Progressbar);
|
Vue.component('Progressbar', Progressbar);
|
||||||
Vue.component('Progresseditor', Progresseditor);
|
Vue.component('Progresseditor', Progresseditor);
|
||||||
new Vue().$mount('#app');
|
new Vue({store}).$mount('#app');
|
||||||
78
assets/js/pages/booklisting.vue
Normal file
78
assets/js/pages/booklisting.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<input type="text" class="form-control" placeholder="search by title" v-model="searchTerm" @keydown.enter="search"/>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Rating</th>
|
||||||
|
<th>Publisher</th>
|
||||||
|
<th>Publish date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="books.length" v-for="book in books">
|
||||||
|
<td>
|
||||||
|
<a :href="'/book/'+book.id">
|
||||||
|
<img class="img-thumbnail" style="max-width: 80px" :src="'/book_covers/cover_'+book.id+'.jpg'"/>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td><a :href="'/book/'+book.id" class="text-decoration-none">{{ book.title }}</a></td>
|
||||||
|
<td>{{ book.short_description }}</td>
|
||||||
|
<td>{{ '⭐'.repeat(book.rating) }}</td>
|
||||||
|
<td>{{ book.publisher }}</td>
|
||||||
|
<td>{{ book.publish_date }}</td>
|
||||||
|
<td>
|
||||||
|
<a :href="'/book/'+book.id" class="text-decoration-none">show</a>
|
||||||
|
<a :href="'/book/'+book.id+'/edit/'" class="text-decoration-none">edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td colspan="9">no records found</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapActions, mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BookListing',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchTerm: null,
|
||||||
|
selectedBook: null,
|
||||||
|
manualMode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState('booksmodule', ['books']),
|
||||||
|
...mapState('usermodule', ['user']),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('booksmodule', [
|
||||||
|
'findAll'
|
||||||
|
]),
|
||||||
|
search: function () {
|
||||||
|
this.updateHistory();
|
||||||
|
this.findAll(this.searchTerm);
|
||||||
|
},
|
||||||
|
updateHistory: function () {
|
||||||
|
if (this.searchTerm && history.pushState) {
|
||||||
|
let url = window.location.protocol + "//" + window.location.host + window.location.pathname + '?search=' + this.searchTerm;
|
||||||
|
window.history.pushState({path: url}, '', url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
this.searchTerm = urlParams.get('search') || "";
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
23
assets/js/pages/booklistingheader.vue
Normal file
23
assets/js/pages/booklistingheader.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 v-if="isSearching" v-html="'Book search: '+searchTitle"></h1>
|
||||||
|
<h1 v-else>Book list</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {mapGetters, mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BookListingHeader',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchTerm: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState('booksmodule', ['searchTitle']),
|
||||||
|
...mapGetters('booksmodule', ['isSearching'])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -15,67 +15,73 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="file in files">
|
<tr v-for="file in files">
|
||||||
<td>{{ file.fileName }}
|
<td>{{ file.file_name }}
|
||||||
<p class="mb-0" v-if="file.book">
|
<p class="mb-0" v-if="file.book">
|
||||||
<a class="text-decoration-none" :href="'/book/'+file.book.id">{{
|
<a class="text-decoration-none" :href="'/book/'+file.book.id">{{
|
||||||
file.book.title
|
file.book.title
|
||||||
}}</a> <a href="" class="text-decoration-none link-secondary">{{ file.book.author }}</a>
|
}}</a> <a href="" class="text-decoration-none link-secondary">{{ file.book.author }}</a>
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ formatSize(file.fileSize) }}</td>
|
<td class="text-end">{{ file.file_size_human }}</td>
|
||||||
<td>{{ file.extension }}</td>
|
<td>{{ file.extension }}</td>
|
||||||
<td><a :href="'/file/'+ file.id" class="link-secondary">download</a></td>
|
<td><a :href="'/file/'+ file.id" class="link-secondary">download</a></td>
|
||||||
<td><a :href="'/file/delete/'+file.id" @click.prevent="deleteFile(file.id)" class="link-danger">remove</a>
|
<td><a @click.prevent="deleteFile(file.id)" class="link-danger cursor-pointer">remove</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<vue-dropzone v-if="bookId"
|
||||||
|
ref="myVueDropzone"
|
||||||
|
id="dropzone"
|
||||||
|
:options="dropzoneOptions"
|
||||||
|
:useCustomSlot="true"
|
||||||
|
@vdropzone-success="vdropzoneSuccess"
|
||||||
|
>
|
||||||
|
<div class="dropzone-custom-content">
|
||||||
|
<h3 class="dropzone-custom-title">Drag and drop to upload content!</h3>
|
||||||
|
<div class="subtitle">
|
||||||
|
...or click to select a file from your computer
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</vue-dropzone>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios';
|
import vue2Dropzone from "vue2-dropzone";
|
||||||
import {EventBus} from "../event-bus";
|
import {mapActions, mapState} from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Files',
|
name: 'Files',
|
||||||
components: {
|
components: {
|
||||||
EventBus
|
vueDropzone: vue2Dropzone
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
files: []
|
dropzoneOptions: {
|
||||||
|
capture: 'image/',
|
||||||
|
url: '/book/' + this.bookId,
|
||||||
|
thumbnailWidth: 150,
|
||||||
|
maxFilesize: 50.5,
|
||||||
|
acceptedFiles: '.pdf, .epub, .mobi'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
bookId: {type: Number, default: null}
|
bookId: {type: Number, default: null}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getFiles();
|
this.getFiles(this.bookId);
|
||||||
},
|
},
|
||||||
created() {
|
computed: {
|
||||||
window.EventBus.$on('fileUploaded', this.getFiles);
|
...mapState('filemodule', ['files'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deleteFile: function (fileId) {
|
...mapActions('filemodule', ['getFiles', 'deleteFile']),
|
||||||
axios.get(window.location.origin + '/file/delete/' + fileId).then(() => this.getFiles())
|
vdropzoneSuccess(file) {
|
||||||
},
|
this.$refs.myVueDropzone.removeFile(file);
|
||||||
formatSize: function (bytes) {
|
this.getFiles(this.bookId);
|
||||||
if (bytes === 0) {
|
|
||||||
return "0.00 B";
|
|
||||||
}
|
|
||||||
const e = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
||||||
return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B';
|
|
||||||
},
|
|
||||||
getFiles: function () {
|
|
||||||
axios.get(this.getFilesEndpoint()).then(response => this.files = response.data)
|
|
||||||
},
|
|
||||||
getFilesEndpoint: function () {
|
|
||||||
if (this.bookId) {
|
|
||||||
return '/book/' + this.bookId + '/files';
|
|
||||||
}
|
|
||||||
return '/file/all';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="progress" style="height: 2px;">
|
<div class="progress" style="height: 2px;">
|
||||||
<div class="progress-bar" role="progressbar" :style="{width:progress+'%'}"></div>
|
<div class="progress-bar" role="progressbar" :style="{width:progressPercent+'%'}"></div>
|
||||||
<div class="progress-bar bg-secondary bg-opacity-10" role="progressbar" :style="{width:100-progress+'%'}"></div>
|
<div class="progress-bar bg-secondary bg-opacity-10" role="progressbar"
|
||||||
|
:style="{width:100-progressPercent+'%'}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {EventBus} from "../event-bus";
|
import {UPDATING_PROGRESS_SUCCESS} from '../store/mutation-types'
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Progressbar',
|
name: 'Progressbar',
|
||||||
components: {
|
|
||||||
EventBus
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
totalPages: Number,
|
totalPages: Number,
|
||||||
readPages: Number
|
readPages: Number
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
newProgress: this.readPages
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
progress() {
|
...mapState('bookprogressmodule', ['progress']),
|
||||||
return Math.round(this.newProgress / this.totalPages * 100);
|
progressPercent() {
|
||||||
|
return Math.round(this.progress / this.totalPages * 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
},
|
|
||||||
created() {
|
created() {
|
||||||
window.EventBus.$on('updateProgress', (data) => {
|
this.$store.commit('bookprogressmodule/' + UPDATING_PROGRESS_SUCCESS, this.readPages);
|
||||||
this.newProgress = data.readPages;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
methods: {}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<span @click="edit" v-if="!editmode">
|
<span @click="edit" v-if="!isEditMode">
|
||||||
{{ newProgress }} pages | {{ progress }}%
|
{{ newProgress }} pages | {{ progress }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-show="editmode">
|
<span v-show="isEditMode">
|
||||||
<input type="number" @keydown.esc="cancelEdit" @keydown.enter="submit" ref="readPagesInput" min="0"
|
<input type="number" @keydown.esc="cancelEdit" @keydown.enter="submit" ref="readPagesInput" min="0"
|
||||||
:max="totalPages" v-model.number="newProgress"/>
|
:max="totalPages" v-model.number="newProgress"/>
|
||||||
</span>
|
</span>
|
||||||
@@ -11,14 +11,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {EventBus} from "../event-bus";
|
import {mapActions, mapGetters} from "vuex";
|
||||||
import axios from "axios";
|
import {TURN_ON_EDIT_MODE, TURN_OFF_EDIT_MODE} from '../store/mutation-types'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Progresseditor',
|
name: 'Progresseditor',
|
||||||
components: {
|
|
||||||
EventBus
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
totalPages: Number,
|
totalPages: Number,
|
||||||
readPages: Number,
|
readPages: Number,
|
||||||
@@ -26,40 +23,32 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editmode: false,
|
|
||||||
newProgress: this.readPages
|
newProgress: this.readPages
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
progress() {
|
progress() {
|
||||||
return Math.round(this.newProgress / this.totalPages * 100);
|
return Math.round(this.newProgress / this.totalPages * 100);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mounted() {
|
...mapGetters('bookprogressmodule', ['isEditMode'])
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.newProgress = this.readPages;
|
this.newProgress = this.readPages;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions('bookprogressmodule', [
|
||||||
|
'updateProgress'
|
||||||
|
]),
|
||||||
edit: function () {
|
edit: function () {
|
||||||
this.editmode = true;
|
this.$store.commit('bookprogressmodule/' + TURN_ON_EDIT_MODE);
|
||||||
setTimeout(() => this.$refs.readPagesInput.focus(), 1)
|
setTimeout(() => this.$refs.readPagesInput.focus(), 1)
|
||||||
},
|
},
|
||||||
cancelEdit: function () {
|
cancelEdit: function () {
|
||||||
this.editmode = false;
|
this.$store.commit('bookprogressmodule/' + TURN_OFF_EDIT_MODE);
|
||||||
},
|
},
|
||||||
submit: function () {
|
submit: function () {
|
||||||
this.editmode = false;
|
this.$store.commit('bookprogressmodule/' + TURN_OFF_EDIT_MODE);
|
||||||
axios.post('/progress/update', {
|
this.updateProgress({'bookId': this.bookId, 'progress': this.newProgress});
|
||||||
bookId: this.bookId,
|
|
||||||
progress: this.newProgress
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
'content-type': 'text/json'
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
window.EventBus.$emit('updateProgress', {readPages: this.newProgress});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
assets/js/store/index.js
Normal file
18
assets/js/store/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import booksmodule from './modules/books';
|
||||||
|
import bookprogressmodule from './modules/bookprogress';
|
||||||
|
import filemodule from './modules/filemodule';
|
||||||
|
import usermodule from './modules/usermodule';
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
modules:{
|
||||||
|
booksmodule,
|
||||||
|
bookprogressmodule,
|
||||||
|
filemodule,
|
||||||
|
usermodule
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
70
assets/js/store/modules/bookprogress.js
Normal file
70
assets/js/store/modules/bookprogress.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import BookApi from "../../api/book";
|
||||||
|
import {
|
||||||
|
UPDATING_PROGRESS,
|
||||||
|
UPDATING_PROGRESS_SUCCESS,
|
||||||
|
UPDATING_PROGRESS_ERROR,
|
||||||
|
TURN_ON_EDIT_MODE,
|
||||||
|
TURN_OFF_EDIT_MODE
|
||||||
|
} from '../mutation-types.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
isUpdating: false,
|
||||||
|
error: null,
|
||||||
|
progress: null,
|
||||||
|
editMode: false
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isEditMode(state) {
|
||||||
|
return state.editMode;
|
||||||
|
},
|
||||||
|
isUpdating(state) {
|
||||||
|
return state.isUpdating;
|
||||||
|
},
|
||||||
|
hasError(state) {
|
||||||
|
return state.error !== null;
|
||||||
|
},
|
||||||
|
error(state) {
|
||||||
|
return state.error;
|
||||||
|
},
|
||||||
|
progress(state) {
|
||||||
|
return state.progress;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
[UPDATING_PROGRESS](state) {
|
||||||
|
state.isUpdating = true;
|
||||||
|
state.error = null;
|
||||||
|
state.progress = null;
|
||||||
|
},
|
||||||
|
[UPDATING_PROGRESS_SUCCESS](state, progress) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.progress = progress;
|
||||||
|
},
|
||||||
|
[UPDATING_PROGRESS_ERROR](state, error) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = error;
|
||||||
|
state.progress = null;
|
||||||
|
},
|
||||||
|
[TURN_ON_EDIT_MODE](state) {
|
||||||
|
state.editMode = true;
|
||||||
|
},
|
||||||
|
[TURN_OFF_EDIT_MODE](state) {
|
||||||
|
state.editMode = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async updateProgress({commit}, data) {
|
||||||
|
commit(UPDATING_PROGRESS);
|
||||||
|
try {
|
||||||
|
let response = await BookApi.bookUpdateProgress(data.bookId, data.progress);
|
||||||
|
commit(UPDATING_PROGRESS_SUCCESS, response.data.progress);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
commit(UPDATING_PROGRESS_ERROR, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
72
assets/js/store/modules/books.js
Normal file
72
assets/js/store/modules/books.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import BookApi from "../../api/book";
|
||||||
|
|
||||||
|
const
|
||||||
|
FETCHING_BOOKS = "FETCHING_BOOKS",
|
||||||
|
FETCHING_BOOKS_SUCCESS = "FETCHING_BOOKS_SUCCESS",
|
||||||
|
FETCHING_BOOKS_ERROR = "FETCHING_BOOKS_ERROR",
|
||||||
|
SETTING_SEARCH_TERM = "SETTING_SEARCH_TERM"
|
||||||
|
;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
books: [],
|
||||||
|
searchTitle: null
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isLoading(state) {
|
||||||
|
return state.isLoading;
|
||||||
|
},
|
||||||
|
hasError(state) {
|
||||||
|
return state.error !== null;
|
||||||
|
},
|
||||||
|
error(state) {
|
||||||
|
return state.error;
|
||||||
|
},
|
||||||
|
hasBooks(state) {
|
||||||
|
return state.books.length > 0;
|
||||||
|
},
|
||||||
|
books(state) {
|
||||||
|
return state.books;
|
||||||
|
},
|
||||||
|
isSearching(state) {
|
||||||
|
return !!state.searchTitle;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
[SETTING_SEARCH_TERM](state, searchTitle) {
|
||||||
|
state.searchTitle = searchTitle;
|
||||||
|
},
|
||||||
|
[FETCHING_BOOKS](state) {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
state.books = [];
|
||||||
|
},
|
||||||
|
[FETCHING_BOOKS_SUCCESS](state, books) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.books = books;
|
||||||
|
},
|
||||||
|
[FETCHING_BOOKS_ERROR](state, error) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = error;
|
||||||
|
state.books = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async findAll({commit, state}, searchTitle) {
|
||||||
|
commit(FETCHING_BOOKS);
|
||||||
|
try {
|
||||||
|
commit(SETTING_SEARCH_TERM, searchTitle);
|
||||||
|
let response = await BookApi.booksFetch(state.searchTitle);
|
||||||
|
commit(FETCHING_BOOKS_SUCCESS, response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
commit(FETCHING_BOOKS_ERROR, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
68
assets/js/store/modules/filemodule.js
Normal file
68
assets/js/store/modules/filemodule.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import FileApi from "../../api/file";
|
||||||
|
import {
|
||||||
|
REMOVE_FILE_FROM_LIST,
|
||||||
|
FETCHING_FILES,
|
||||||
|
FETCHING_FILES_SUCCESS,
|
||||||
|
FETCHING_FILES_ERROR
|
||||||
|
} from '../mutation-types.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
files: []
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isLoading(state) {
|
||||||
|
return state.isLoading;
|
||||||
|
},
|
||||||
|
hasError(state) {
|
||||||
|
return state.error !== null;
|
||||||
|
},
|
||||||
|
error(state) {
|
||||||
|
return state.error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
[FETCHING_FILES](state) {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
[FETCHING_FILES_SUCCESS](state, files) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.files = files;
|
||||||
|
},
|
||||||
|
[FETCHING_FILES_ERROR](state, error) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = error;
|
||||||
|
state.files = [];
|
||||||
|
},
|
||||||
|
[REMOVE_FILE_FROM_LIST](state, fileId) {
|
||||||
|
|
||||||
|
const index = state.files.findIndex(file => file.id === fileId);
|
||||||
|
state.files.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async deleteFile({commit}, fileId) {
|
||||||
|
try {
|
||||||
|
await FileApi.deleteFile(fileId);
|
||||||
|
commit(REMOVE_FILE_FROM_LIST, fileId)
|
||||||
|
} catch (error) {
|
||||||
|
commit(FETCHING_FILES_ERROR, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getFiles({commit}, bookId) {
|
||||||
|
commit(FETCHING_FILES);
|
||||||
|
try {
|
||||||
|
let response = await FileApi.getFiles(bookId);
|
||||||
|
commit(FETCHING_FILES_SUCCESS, response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
commit(FETCHING_FILES_ERROR, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
83
assets/js/store/modules/usermodule.js
Normal file
83
assets/js/store/modules/usermodule.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import UserApi from "../../api/user";
|
||||||
|
import {
|
||||||
|
LOGIN_START,
|
||||||
|
LOGIN_SUCCESS,
|
||||||
|
LOGIN_ERROR,
|
||||||
|
STORE_USER_INFO,
|
||||||
|
LOGIN_STOP,
|
||||||
|
} from '../mutation-types.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
user: window.user,
|
||||||
|
userUri: null,
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
isLoading(state) {
|
||||||
|
return state.isLoading;
|
||||||
|
},
|
||||||
|
hasError(state) {
|
||||||
|
return state.error !== null;
|
||||||
|
},
|
||||||
|
error(state) {
|
||||||
|
return state.error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
[LOGIN_START](state) {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.error = null;
|
||||||
|
state.user = null;
|
||||||
|
},
|
||||||
|
[LOGIN_SUCCESS](state, userUri) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.userUri = userUri;
|
||||||
|
},
|
||||||
|
[LOGIN_STOP](state, userUri) {
|
||||||
|
state.isLoading = false;
|
||||||
|
},
|
||||||
|
[STORE_USER_INFO](state, user) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = null;
|
||||||
|
state.user = user;
|
||||||
|
},
|
||||||
|
[LOGIN_ERROR](state, error) {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.error = error;
|
||||||
|
state.user = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async login({dispatch, commit}, data) {
|
||||||
|
console.info(data.email);
|
||||||
|
console.info(data.password);
|
||||||
|
commit(LOGIN_START);
|
||||||
|
let response = await UserApi.login(data.email, data.password)
|
||||||
|
.then(response => {
|
||||||
|
console.log(response.data);
|
||||||
|
console.log(response.headers.location);
|
||||||
|
// commit(LOGIN_SUCCESS,response.data);
|
||||||
|
dispatch('getUserInfo', response.headers.location)
|
||||||
|
commit(LOGIN_SUCCESS, response.headers.location);
|
||||||
|
//this.$emit('user-authenticated', userUri);
|
||||||
|
}).catch(error => {
|
||||||
|
if (error.response.data.error) {
|
||||||
|
|
||||||
|
commit(LOGIN_ERROR, error.response.data.error);
|
||||||
|
console.log(error.response.data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}).finally(() => {
|
||||||
|
commit(LOGIN_STOP);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async getUserInfo({commit}, userUri) {
|
||||||
|
let userInfo = await UserApi.getUserData(userUri);
|
||||||
|
commit(STORE_USER_INFO, userInfo.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
18
assets/js/store/mutation-types.js
Normal file
18
assets/js/store/mutation-types.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const
|
||||||
|
UPDATING_PROGRESS = "UPDATING_PROGRESS",
|
||||||
|
UPDATING_PROGRESS_SUCCESS = "UPDATING_PROGRESS_SUCCESS",
|
||||||
|
UPDATING_PROGRESS_ERROR = "UPDATING_PROGRESS_ERROR",
|
||||||
|
TURN_ON_EDIT_MODE = "TURN_ON_EDIT_MODE",
|
||||||
|
TURN_OFF_EDIT_MODE = "TURN_OFF_EDIT_MODE",
|
||||||
|
|
||||||
|
FETCHING_FILES = "FETCHING_FILES",
|
||||||
|
FETCHING_FILES_SUCCESS = "FETCHING_FILES_SUCCESS",
|
||||||
|
FETCHING_FILES_ERROR = "FETCHING_FILES_ERROR",
|
||||||
|
REMOVE_FILE_FROM_LIST = "REMOVE_FILE_FROM_LIST",
|
||||||
|
|
||||||
|
LOGIN_START = "LOGIN_START",
|
||||||
|
LOGIN_SUCCESS = "LOGIN_SUCCESS",
|
||||||
|
LOGIN_ERROR = "LOGIN_ERROR",
|
||||||
|
LOGIN_STOP = "LOGIN_STOP",
|
||||||
|
STORE_USER_INFO = "STORE_USER_INFO"
|
||||||
|
;
|
||||||
6
assets/js/user.js
Normal file
6
assets/js/user.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import Loginform from "./components/loginform";
|
||||||
|
import store from "./store/index";
|
||||||
|
|
||||||
|
Vue.component('Loginform', Loginform);
|
||||||
|
new Vue({store}).$mount('#app');
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "~bootstrap";
|
@import "~bootstrap";
|
||||||
|
@import "vue2-dropzone/dist/vue2Dropzone.min.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
//background-color: lightblue !important;
|
//background-color: lightblue !important;
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
"symfony/yaml": "6.0.*",
|
"symfony/yaml": "6.0.*",
|
||||||
"techtube/bookinfo": "1.*",
|
"techtube/bookinfo": "1.*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0",
|
||||||
|
"ext-mbstring": "*"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ security:
|
|||||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
main:
|
||||||
|
json_login:
|
||||||
|
check_path: app_login_api
|
||||||
|
username_path: email
|
||||||
|
password_path: password
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
custom_authenticator: App\Security\CustomAuthenticator
|
custom_authenticator: App\Security\CustomAuthenticator
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@popperjs/core": "^2.11.5",
|
"@popperjs/core": "^2.11.5",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"popper": "^1.0.1"
|
"popper": "^1.0.1",
|
||||||
|
"vue2-dropzone": "^3.6.0",
|
||||||
|
"vuex": "^3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,15 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
|||||||
class BookController extends AbstractController
|
class BookController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route('/', name: 'app_book_index', methods: ['GET'])]
|
#[Route('/', name: 'app_book_index', methods: ['GET'])]
|
||||||
public function index(BookRepository $bookRepository): Response
|
public function index(BookRepository $bookRepository, Request $request): Response
|
||||||
{
|
{
|
||||||
|
if ($request->query->has('search')) {
|
||||||
|
$books = $bookRepository->findByTitle($request->query->get('search'));
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('book/index.html.twig', [
|
return $this->render('book/index.html.twig', [
|
||||||
'books' => $bookRepository->findAll(),
|
'searchTerm' => $request->query->get('search'),
|
||||||
|
'books' => $books ?? $bookRepository->findAll(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,26 +103,17 @@ class BookController extends AbstractController
|
|||||||
): Response {
|
): Response {
|
||||||
$fileForm = $this->createForm(FileType::class);
|
$fileForm = $this->createForm(FileType::class);
|
||||||
$fileForm->handleRequest($request);
|
$fileForm->handleRequest($request);
|
||||||
|
if ($fileForm->isSubmitted()) {
|
||||||
if ($fileForm->isSubmitted() && $fileForm->isValid()) {
|
/** @var UploadedFile $ebook */
|
||||||
/** @var UploadedFile[] $ebooks */
|
$ebook = $request->files->get('file');
|
||||||
$ebook = $request->files->get('file')['file'];
|
|
||||||
$fileService->saveFile($ebook, $book);
|
$fileService->saveFile($ebook, $book);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->renderForm('book/show.html.twig', [
|
return $this->render('book/show.html.twig', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'file_form' => $fileForm
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/{id}/files', name: 'app_book_file', methods: ['GET'])]
|
|
||||||
public function files(Book $book): JsonResponse
|
|
||||||
{
|
|
||||||
return $this->json($book->getFiles(), context: [AbstractNormalizer::IGNORED_ATTRIBUTES => ['book']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[Route('/{id}/edit', name: 'app_book_edit', methods: ['GET', 'POST'])]
|
#[Route('/{id}/edit', name: 'app_book_edit', methods: ['GET', 'POST'])]
|
||||||
public function edit(
|
public function edit(
|
||||||
Request $request,
|
Request $request,
|
||||||
|
|||||||
18
src/Controller/IndexController.php
Normal file
18
src/Controller/IndexController.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class IndexController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route('/', name: 'app_index')]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('index/index.html.twig', [
|
||||||
|
'controller_name' => 'IndexController',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,19 +2,22 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use ApiPlatform\Core\Api\IriConverterInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||||
|
use Symfony\Component\Validator\Constraints\Json;
|
||||||
|
|
||||||
class SecurityController extends AbstractController
|
class SecurityController extends AbstractController
|
||||||
{
|
{
|
||||||
#[Route(path: '/login', name: 'app_login')]
|
#[Route(path: '/login', name: 'app_login')]
|
||||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||||
{
|
{
|
||||||
// if ($this->getUser()) {
|
if ($this->getUser()) {
|
||||||
// return $this->redirectToRoute('target_path');
|
return $this->redirectToRoute('app_book_index');
|
||||||
// }
|
}
|
||||||
|
|
||||||
// get the login error if there is one
|
// get the login error if there is one
|
||||||
$error = $authenticationUtils->getLastAuthenticationError();
|
$error = $authenticationUtils->getLastAuthenticationError();
|
||||||
@@ -24,9 +27,23 @@ class SecurityController extends AbstractController
|
|||||||
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
|
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/login_api', name: 'app_login_api', methods: ['POST'])]
|
||||||
|
public function login_api(IriConverterInterface $iriConverter): Response
|
||||||
|
{
|
||||||
|
return new Response(null, 204, [
|
||||||
|
'Location' => $iriConverter->getIriFromItem($this->getUser())
|
||||||
|
]);
|
||||||
|
// return $this->json([
|
||||||
|
// 'user' => $this->getUser() ? $this->getUser()->getId() : null
|
||||||
|
// ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[Route(path: '/logout', name: 'app_logout')]
|
#[Route(path: '/logout', name: 'app_logout')]
|
||||||
public function logout(): void
|
public function logout(): void
|
||||||
{
|
{
|
||||||
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
|
throw new \LogicException(
|
||||||
|
'This method can be blank - it will be intercepted by the logout key on your firewall.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,33 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Core\Annotation\ApiFilter;
|
||||||
|
use ApiPlatform\Core\Annotation\ApiResource;
|
||||||
|
use ApiPlatform\Core\Annotation\ApiSubresource;
|
||||||
|
use ApiPlatform\Core\Bridge\Doctrine\Common\Filter\SearchFilterInterface;
|
||||||
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
|
||||||
use App\Repository\BookRepository;
|
use App\Repository\BookRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Context;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
|
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
||||||
|
|
||||||
|
#[ApiResource]
|
||||||
#[ORM\Entity(repositoryClass: BookRepository::class)]
|
#[ORM\Entity(repositoryClass: BookRepository::class)]
|
||||||
|
#[ApiFilter(SearchFilter::class, properties: ['title' => SearchFilterInterface::STRATEGY_PARTIAL])]
|
||||||
class Book
|
class Book
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $title;
|
private $title;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
@@ -28,11 +41,13 @@ class Book
|
|||||||
private $publisher;
|
private $publisher;
|
||||||
|
|
||||||
#[ORM\Column(type: 'date', nullable: true)]
|
#[ORM\Column(type: 'date', nullable: true)]
|
||||||
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
||||||
private $publish_date;
|
private $publish_date;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||||
private $subtitle;
|
private $subtitle;
|
||||||
|
|
||||||
|
#[ApiSubresource]
|
||||||
#[ORM\OneToMany(mappedBy: 'book', targetEntity: File::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'book', targetEntity: File::class, orphanRemoval: true)]
|
||||||
private $files;
|
private $files;
|
||||||
|
|
||||||
@@ -46,6 +61,7 @@ class Book
|
|||||||
private $volume;
|
private $volume;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $author;
|
private $author;
|
||||||
|
|
||||||
#[ORM\Column(type: 'text', nullable: true)]
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
@@ -104,6 +120,12 @@ class Book
|
|||||||
return $this->description;
|
return $this->description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[SerializedName('short_description')]
|
||||||
|
public function getShortDescription(): ?string
|
||||||
|
{
|
||||||
|
return mb_strimwidth($this->description, 0, 150, "...");;
|
||||||
|
}
|
||||||
|
|
||||||
public function setDescription(?string $description): self
|
public function setDescription(?string $description): self
|
||||||
{
|
{
|
||||||
$this->description = $description;
|
$this->description = $description;
|
||||||
|
|||||||
@@ -5,23 +5,28 @@ namespace App\Entity;
|
|||||||
use ApiPlatform\Core\Annotation\ApiResource;
|
use ApiPlatform\Core\Annotation\ApiResource;
|
||||||
use App\Repository\FileRepository;
|
use App\Repository\FileRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
|
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||||
|
|
||||||
#[ApiResource]
|
#[ApiResource(collectionOperations: ['get'], itemOperations: ['get'], denormalizationContext: ['groups' => ['file:write']], normalizationContext: ['groups' => ['file:read']])]
|
||||||
#[ORM\Entity(repositoryClass: FileRepository::class)]
|
#[ORM\Entity(repositoryClass: FileRepository::class)]
|
||||||
class File
|
class File
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $file_name;
|
private $file_name;
|
||||||
|
|
||||||
#[ORM\Column(type: 'integer')]
|
#[ORM\Column(type: 'integer')]
|
||||||
private $file_size;
|
private $file_size;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $extension;
|
private $extension;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 255)]
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
@@ -29,6 +34,7 @@ class File
|
|||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: Book::class, inversedBy: 'files')]
|
#[ORM\ManyToOne(targetEntity: Book::class, inversedBy: 'files')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Groups('file:read')]
|
||||||
private $book;
|
private $book;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -53,6 +59,15 @@ class File
|
|||||||
return $this->file_size;
|
return $this->file_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Groups('file:read')]
|
||||||
|
#[SerializedName('file_size_human')]
|
||||||
|
public function getFileSizeHuman(): string
|
||||||
|
{
|
||||||
|
$base = log($this->file_size) / log(1024);
|
||||||
|
$suffix = array("B", "KB", "MB", "GB", "TB")[floor($base)];
|
||||||
|
return round(pow(1024, $base - floor($base)), 2) . $suffix;
|
||||||
|
}
|
||||||
|
|
||||||
public function setFileSize(int $file_size): self
|
public function setFileSize(int $file_size): self
|
||||||
{
|
{
|
||||||
$this->file_size = $file_size;
|
$this->file_size = $file_size;
|
||||||
|
|||||||
@@ -2,11 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Core\Annotation\ApiResource;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(collectionOperations: [
|
||||||
|
"get",
|
||||||
|
"post" => ["security" => "is_granted('ROLE_USER')"]
|
||||||
|
], normalizationContext: ['groups' => ['user:read']])]
|
||||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||||
#[ORM\Table(name: '`user`')]
|
#[ORM\Table(name: '`user`')]
|
||||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
@@ -17,9 +23,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
#[ORM\Column(type: 'string', length: 180, unique: true)]
|
||||||
|
#[Groups('user:read')]
|
||||||
private $email;
|
private $email;
|
||||||
|
|
||||||
#[ORM\Column(type: 'json')]
|
#[ORM\Column(type: 'json')]
|
||||||
|
#[Groups('user:read')]
|
||||||
private $roles = [];
|
private $roles = [];
|
||||||
|
|
||||||
#[ORM\Column(type: 'string')]
|
#[ORM\Column(type: 'string')]
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ class BookType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'attr' => ['accept' => ".pdf, .epub, .mobi"]
|
'attr' => ['accept' => ".pdf, .epub, .mobi"]
|
||||||
])
|
])
|
||||||
// ->add('cover', FileType::class, ['mapped' => false, 'data_class' => null, 'required' => false])
|
|
||||||
->add('cover_url', TextType::class, ['mapped' => false, 'help' => 'Fill in the field with a link to a cover image to use it as a cover for the book.', 'label'=> 'Cover url', 'required' => false])
|
->add('cover_url', TextType::class, ['mapped' => false, 'help' => 'Fill in the field with a link to a cover image to use it as a cover for the book.', 'label'=> 'Cover url', 'required' => false])
|
||||||
->add('cover', DropzoneType::class, ['mapped' => false, 'data_class' => null, 'required' => false, 'attr' => ['accept' => "image/*", 'placeholder' => 'Drag and drop or browse']])
|
->add('cover', FileType::class, ['mapped' => false, 'data_class' => null, 'required' => false, 'attr' => ['accept' => "image/*"]])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class FileType extends AbstractType
|
|||||||
'data_class' => null,
|
'data_class' => null,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'attr' => [
|
'attr' => [
|
||||||
'data-controller' => 'filedropzone',
|
|
||||||
'accept' => ".pdf, .epub, .mobi",
|
'accept' => ".pdf, .epub, .mobi",
|
||||||
'placeholder' => 'Drag and drop or browse'
|
'placeholder' => 'Drag and drop or browse'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ class CustomAuthenticator extends AbstractLoginFormAuthenticator
|
|||||||
return new RedirectResponse($targetPath);
|
return new RedirectResponse($targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For example:
|
return new RedirectResponse($this->urlGenerator->generate('app_book_index'));
|
||||||
// return new RedirectResponse($this->urlGenerator->generate('some_route'));
|
|
||||||
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getLoginUrl(Request $request): string
|
protected function getLoginUrl(Request $request): string
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace App\Twig;
|
|||||||
|
|
||||||
use App\Form\SearchType;
|
use App\Form\SearchType;
|
||||||
use Symfony\Component\Form\FormFactoryInterface;
|
use Symfony\Component\Form\FormFactoryInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
@@ -12,11 +14,18 @@ use Twig\TwigFunction;
|
|||||||
class AppExtension extends AbstractExtension
|
class AppExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
|
|
||||||
private $formFactory;
|
private FormFactoryInterface $formFactory;
|
||||||
|
private SerializerInterface $serializer;
|
||||||
|
private TokenStorageInterface $tokenStorage;
|
||||||
|
|
||||||
public function __construct(FormFactoryInterface $formFactory)
|
public function __construct(
|
||||||
{
|
FormFactoryInterface $formFactory,
|
||||||
|
SerializerInterface $serializer,
|
||||||
|
TokenStorageInterface $tokenStorage
|
||||||
|
) {
|
||||||
$this->formFactory = $formFactory;
|
$this->formFactory = $formFactory;
|
||||||
|
$this->serializer = $serializer;
|
||||||
|
$this->tokenStorage = $tokenStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFunctions()
|
public function getFunctions()
|
||||||
@@ -24,6 +33,7 @@ class AppExtension extends AbstractExtension
|
|||||||
return [
|
return [
|
||||||
new TwigFunction('file_exists', [$this, 'file_exists']),
|
new TwigFunction('file_exists', [$this, 'file_exists']),
|
||||||
new TwigFunction('render_search_form', [$this, 'render_search_form']),
|
new TwigFunction('render_search_form', [$this, 'render_search_form']),
|
||||||
|
new TwigFunction('get_user', [$this, 'get_user']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +60,9 @@ class AppExtension extends AbstractExtension
|
|||||||
{
|
{
|
||||||
return $this->formFactory->create(SearchType::class)->createView();
|
return $this->formFactory->create(SearchType::class)->createView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_user()
|
||||||
|
{
|
||||||
|
return $this->serializer->serialize($this->tokenStorage->getToken()?->getUser(), 'jsonld');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,9 @@
|
|||||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||||
<link rel="icon"
|
<link rel="icon"
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>📔</text></svg>">
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>📔</text></svg>">
|
||||||
|
<script>
|
||||||
|
window.user = {{ get_user() | raw }};
|
||||||
|
</script>
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
{{ encore_entry_link_tags('app') }}
|
{{ encore_entry_link_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,53 +3,20 @@
|
|||||||
{% block title %}Book index{% endblock %}
|
{% block title %}Book index{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<div id="app">
|
||||||
<div class="d-flex flex-row justify-content-between align-items-center">
|
<div class="d-flex flex-row justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1>Book list</h1>
|
<book-listing-header></book-listing-header>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ path('app_book_new') }}" class="btn btn-primary">Add new</a>
|
<a href="{{ path('app_book_new') }}" class="btn btn-primary">Add new</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<book-listing></book-listing>
|
||||||
<table class="table">
|
</div>
|
||||||
<thead>
|
{% endblock %}
|
||||||
<tr>
|
|
||||||
<th></th>
|
{% block javascripts %}
|
||||||
<th>Title</th>
|
{{ encore_entry_script_tags('book') }}
|
||||||
<th>Description</th>
|
|
||||||
<th>Publisher</th>
|
|
||||||
<th>Publish date</th>
|
|
||||||
<th>actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for book in books %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if file_exists(asset('book_covers/cover_' ~ book.id ~ '.jpg')) %}
|
|
||||||
<a href="{{ path('app_book_show', {'id': book.id}) }}"><img class="img-thumbnail"
|
|
||||||
style="max-width: 80px"
|
|
||||||
src="{{ asset('book_covers/cover_' ~ book.id ~ '.jpg' ) }}"/>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td><a href="{{ path('app_book_show', {'id': book.id}) }}"
|
|
||||||
class="text-decoration-none">{{ book.title }}</a></td>
|
|
||||||
<td>{{ book.description | slice(0, 200) }}</td>
|
|
||||||
<td>{{ book.publisher }}</td>
|
|
||||||
<td>{{ book.publishDate ? book.publishDate|date('Y-m-d') : '' }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ path('app_book_show', {'id': book.id}) }}" class="text-decoration-none">show</a>
|
|
||||||
<a href="{{ path('app_book_edit', {'id': book.id}) }}" class="text-decoration-none">edit</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="9">no records found</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1>{{ book.title }}</h1>
|
<h1>{{ book.title }}</h1>
|
||||||
<h3>{{ book.subtitle }}</h3>
|
<h3>{{ book.subtitle }}</h3>
|
||||||
<p>{{ book.author }}</p>
|
<p>{{ book.author }}</p>
|
||||||
<p>{% for i in range(1, book.rating) %}⭐{% endfor %}</p>
|
<p>{% if book.rating %}{% for i in range(1, book.rating) %}⭐{% endfor %}{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
<files :book-id="{{ book.id }}"></files>
|
<files :book-id="{{ book.id }}"></files>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ form_start(file_form) }}
|
{# {{ form_start(file_form) }}#}
|
||||||
{{ form_widget(file_form) }}
|
{# {{ form_widget(file_form) }}#}
|
||||||
{{ form_end(file_form) }}
|
{# {{ form_end(file_form) }}#}
|
||||||
|
|
||||||
<a href="{{ path('app_book_index') }}">back to list</a>
|
<a href="{{ path('app_book_index') }}">back to list</a>
|
||||||
|
|
||||||
|
|||||||
27
templates/index/index.html.twig
Normal file
27
templates/index/index.html.twig
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Hello IndexController!{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<style>
|
||||||
|
.example-wrapper {
|
||||||
|
margin: 1em auto;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 95%;
|
||||||
|
font: 18px/1.5 sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-wrapper code {
|
||||||
|
background: #F5F5F5;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<loginform></loginform>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ encore_entry_script_tags('user') }}
|
||||||
|
{% endblock %}
|
||||||
@@ -23,6 +23,7 @@ Encore
|
|||||||
.addEntry('app', './assets/js/app.js')
|
.addEntry('app', './assets/js/app.js')
|
||||||
.addEntry('files', './assets/js/files.js')
|
.addEntry('files', './assets/js/files.js')
|
||||||
.addEntry('book', './assets/js/book.js')
|
.addEntry('book', './assets/js/book.js')
|
||||||
|
.addEntry('user', './assets/js/user.js')
|
||||||
|
|
||||||
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
|
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
|
||||||
.enableStimulusBridge('./assets/controllers.json')
|
.enableStimulusBridge('./assets/controllers.json')
|
||||||
|
|||||||
17
yarn.lock
17
yarn.lock
@@ -3188,6 +3188,11 @@ domutils@^2.5.2, domutils@^2.8.0:
|
|||||||
domelementtype "^2.2.0"
|
domelementtype "^2.2.0"
|
||||||
domhandler "^4.2.0"
|
domhandler "^4.2.0"
|
||||||
|
|
||||||
|
dropzone@^5.5.1:
|
||||||
|
version "5.9.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-5.9.3.tgz#b3070ae090fa48cbc04c17535635537ca72d70d6"
|
||||||
|
integrity sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==
|
||||||
|
|
||||||
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
|
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
||||||
@@ -7426,11 +7431,23 @@ vue-template-es2015-compiler@^1.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||||
|
|
||||||
|
vue2-dropzone@^3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue2-dropzone/-/vue2-dropzone-3.6.0.tgz#b4bb4b64de1cbbb3b88f04b24878e06780a51546"
|
||||||
|
integrity sha512-YXC1nCWIZvfa98e/i6h+EshZCkFSxFEh0Sxr9ODfThAPPDVhAzLLlz/4XIx0NGO1QeSy6htwSstte47R7vVhLQ==
|
||||||
|
dependencies:
|
||||||
|
dropzone "^5.5.1"
|
||||||
|
|
||||||
vue@^2.5:
|
vue@^2.5:
|
||||||
version "2.6.14"
|
version "2.6.14"
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||||
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
||||||
|
|
||||||
|
vuex@^3:
|
||||||
|
version "3.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||||
|
integrity sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==
|
||||||
|
|
||||||
watchpack@^2.3.1:
|
watchpack@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
||||||
|
|||||||
Reference in New Issue
Block a user