This commit is contained in:
Dmitrii Kollerov
2026-09-27 16:53:51 +07:00
commit 7272a8ec67
32 changed files with 4360 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
<template>
<div id="app">
<header>
<div class="column">
<span class="left-menu">
</span>
<span class="middle-menu">
</span>
<span class="right-menu">
</span>
</div>
</header>
<router-view></router-view>
<footer>
<div class="column">
<div class="footer-left">
d.kollerov
</div>
</div>
</footer>
</div>
</template>
<script>
export default {
name: 'app'
};
</script>
<style>
@font-face{
font-family: "Buran";
src: local("Buran USSR"), url('./assets/fonts/Buran USSR.ttf') format('truetype');
}
@font-face {
font-family: "Mustica";
src: local("MusticaPro-SemiBold"), url('./assets/fonts/MusticaPro-SemiBold.otf') format('opentype');
}
html {
height: 100%;
color: white;
font-family: Mustica;
font-size: 14px;
}
h1, h2, h3, h5, h6 {
font-family: Buran, Arial;
}
h1, h2 {
text-align: center;
}
h1 {
font-size: 30px;
}
h2 {
font-size: 28px;
}
h4 {
font-size: 26px;
}
h5 {
font-size: 22px;
}
h6 {
font-size: 20px
}
hr {
color: white;
}
body {
margin: 0;
padding: 0;
height: 100%;
background-color: #826c55;
}
a {
color: #5e4226;
}
a:visited {
color: #2f2014;
}
div#app {
min-height: 100%;
display: flex;
flex-direction:column;
height: 100%;
}
header {
width: 100%;
height: 40px;
background-color: #5e4226;
font-family: Buran;
font-size: 24px;
}
header .left-menu, header .right-menu {
width: calc((100% - 40px) / 2);
display: inline-flex;
}
header .right-menu {
flex-direction: row-reverse;
}
header .middle-menu {
width: 40px;
display: inline-block;
}
header a, header a:visited {
color: white;
text-decoration: none;
margin-left: 7px;
margin-right: 7px;
text-shadow: 0px 0px 6px #2f2014;
}
header a:hover {
}
header a.router-link-active {
text-shadow: 0px 0px 6px white;
color: #2f2014;
}
.column {
margin-left: 150px;
margin-right: 150px;
}
.background {
background-color: #baa48f;
padding-top: 2px;
padding-bottom: 10px;
margin-left: 5px;
margin-right: 5px;
color: #2f2014;
}
footer {
display: flex;
padding-top: 5px;
padding-bottom: 5px;
margin-top: auto;
align-self: flex-end;
width: 100%;
height: min-content;
background-color: #5e4226;
}
footer .footer-left {
}
</style>
Binary file not shown.
Binary file not shown.
+33
View File
@@ -0,0 +1,33 @@
::: Buy Premium Version & Full Package, please visit :::
--> https://www.myfonts.com/fonts/alifinart-studio/mustica-pro/
::: My Portfolio :::
--> www.behance.net/alifinart/
::: Join our circle or subscribe now to get an extra-special offers :::
--> https://gumroad.com/alifinart
--------------------
Any questions or request please feel free to email:
alifinart@gmail.com
Thank's.
Copyright © 2021 Alifinart
--------------------
If you distribute my work, MUST include the name of the foundry “Alifinart Studio”
and include a link to my portfolio (behance.net/alifinart), unless you have received
permission from us.
Thank you.
+52
View File
@@ -0,0 +1,52 @@
<template>
<div class="home">
<div class="background">
<div class="list-container">
<h2>Books</h2>
<div class="column">
<Dashboard />
</div>
</div>
</div>
</div>
</template>
<script>
import Dashboard from "./admin/Dashboard.vue";
export default {
name: 'Home',
data() {
},
components: {
Dashboard: Dashboard
},
methods: {
},
mounted() {
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h2 {
color: #2f2014;
}
.list {
display: flex;
flex-direction: row;
}
.list-container {
margin-left: -5px;
margin-right: -5px;
}
.list-elem {
width: 25%;
padding: 2px 4px 2px 2px;
}
</style>
+89
View File
@@ -0,0 +1,89 @@
<template>
<div class="game">
<div class="background">
<form @submit.prevent="uploadBook">
<BookEditor v-bind:entry="entry"
v-bind:error="error"
editorTitle="Add book"
:is_edit="false"
@modelEdit="data => onInput(data)"
@error="this.error = $event.message"
/>
</form>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import BookEditor from './elements/BookEditor.vue'
export default {
name: "BookAdd",
data() {
return {
error: null,
entry: {
id: '',
title: '',
author: '',
yearPublished: new Date,
bookIndex: '<root>\n\t<chapter>\n\t\t<name></name><page></page>\n\t</chapter>\n</root>'
},
}
},
components: {
BookEditor
},
computed: mapState({
//token: state => "Bearer " + state.token,
}),
methods: {
uploadBook: function () {
var self = this;
var data = {
"author": self.entry.author,
"title": self.entry.title,
"yearPublished": self.entry.yearPublished,
"bookIndex": self.entry.bookIndex
};
fetch(self.$store.state.config.API_URL + '/Book/', {
method: 'POST',
headers: {
// "Authorization": self.token,
'Content-Type': 'application/json;charset=UTF-8'
},
body: JSON.stringify(data)
}).then(function (response) {
if (response.status == 401 || response.status == 403) {
// this.$store.commit('purgeToken');
// this.$router.push("/admin");
}else{
return response.json();
}
}).then((response) => {
if(response.isSuccess == true)
{
self.$router.push("/");
}else{
self.error = JSON.stringify(response);
}
}).catch(error => {
if (error.status == 401 || error.status == 403) {
// self.$store.commit('purgeToken');
// self.$router.push("/admin");
}
self.error = error;
});
},
onInput(data) {
this.entry = data;
},
},
}
</script>
<style scoped>
</style>
+114
View File
@@ -0,0 +1,114 @@
<template>
<div class="game">
<div class="background">
<form @submit.prevent="saveBook">
<BookEditor v-bind:entry="entry"
:error="error"
editorTitle="Edit game"
:is_edit="true"
@modelEdit="data => onInput(data)"
@error="this.error = $event.message"
/>
</form>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import BookEditor from './elements/BookEditor.vue'
export default {
name: "BookEdit",
data() {
return {
error: null,
entry: {
id: '',
title: '',
author: '',
yearPublished: new Date,
bookIndex: ''
},
}
},
components: {
BookEditor: BookEditor
},
computed: mapState({
// token: state => "Bearer " + state.token,
}),
mounted() {
this.getBook()
},
methods: {
getBook() {
var bookId = this.$route.params.id;
fetch(this.$store.state.config.API_URL + '/Book/' + bookId, {
headers: {
// "Authorization": this.token,
'Content-Type': 'application/json;charset=UTF-8'
}
}).then(function (response) {
if (response.status == 401 || response.status == 403) {
// this.$store.commit('purgeToken');
// this.$router.push("/admin");
}else{
return response.json();
}
}).then(response => {
this.entry.id = response.book.id;
this.entry.title = response.book.title;
this.entry.author = response.book.author;
this.entry.yearPublished = new Date(response.book.yearPublished);
this.entry.bookIndex = response.book.bookIndex;
});
},
saveBook() {
var self = this;
var data = {
"id": self.entry.id,
"author": self.entry.author,
"title": self.entry.title,
"yearPublished": self.entry.yearPublished,
"bookIndex": self.entry.bookIndex
};
fetch(self.$store.state.config.API_URL + '/Book/', {
method: 'PUT',
headers: {
// "Authorization": self.token,
'Content-Type': 'application/json;charset=UTF-8'
},
body: JSON.stringify(data)
}).then(function (response) {
if (response.status == 401 || response.status == 403) {
// this.$store.commit('purgeToken');
// this.$router.push("/admin");
}else{
return response.json();
}
}).then((response) => {
if(response.isSuccess == true)
{
self.$router.push("/");
}else{
self.error = JSON.stringify(response);
}
}).catch(error => {
if (error.status == 401 || error.status == 403) {
// self.$store.commit('purgeToken');
// self.$router.push("/admin");
}
self.error = error;
});
},
onInput(data) {
this.entry = data;
},
},
}
</script>
<style scoped>
</style>
+84
View File
@@ -0,0 +1,84 @@
<template>
<div class="table-wrapper">
<router-link class="addbtn" :to="{ path: '/add-book' }">Add</router-link>
<table>
<thead> <tr> <td>Id</td> <td>Title</td> <td>Author</td> <td>Year published</td> <td>Index</td> <td>Actions</td> </tr> </thead>
<tbody>
<tr v-for="item of bookEntries" v-bind:key="item.id">
<td>{{item.id}}</td>
<td>{{item.title}}</td>
<td>{{item.author}}</td>
<td>{{item.yearPublished}}</td>
<td>{{item.bookIndex}}</td>
<td><router-link :to="{ path: '/edit-book/' + item.id }">Edit</router-link> <button v-on:click="deleteBook(item.id)">Delete</button></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: "BookTable",
data() {
return {
bookEntries: []
}
},
mounted() {
this.getBookList();
},
computed: mapState({
}),
methods: {
getBookList: function () {
var self = this;
fetch(this.$store.state.config.API_URL + '/Book')
.then(function (response) {
return response.json()
}).then(response => {
self.bookEntries = response.books;
});
},
deleteBook(id) {
fetch(this.$store.state.config.API_URL + '/Book/' + id, {
method: 'DELETE',
headers: {
}
}).then(function (response) {
if (response.status == 401 || response.status == 403) {
// this.$store.commit('purgeToken');
// this.$router.push("/admin");
}else{
return response.json()
}
}).then(() => {
this.getBookList();
}).error((error) => {
if (error.status == 401 || error.status == 403) {
// this.$store.commit('purgeToken');
// this.$router.push("/admin");
}
});
}
},
}
</script>
<style scoped>
.addbtn {
font-size: 24px;
}
thead {
background-color: #2f2014;
color: white;
}
tr {
border: dotted 1px #2f2014;
}
td {
border: dotted 1px #2f2014;
}
</style>
+67
View File
@@ -0,0 +1,67 @@
<template>
<div class="admin-dashboard">
<div class="admin-dashboard">
<button :class="{tab: true, active: tabs.indexOf(tab) == selectedTab}" v-bind:key="tab.name" v-for="tab of tabs" v-html="tab.name" v-on:click="selectTab(tabs.indexOf(tab))"></button>
<div class="shown-container">
<keep-alive>
<component v-bind:is="this.tabs[this.selectedTab].component"></component>
</keep-alive>
</div>
</div>
</div>
</template>
<script>
import BookTable from './BookTable.vue';
import { shallowRef } from 'vue'
export default {
name: "Dashboard",
data() {
return {
selectedTab: 0,
tabs: [
{
name: "Books",
component: shallowRef(BookTable)
},
]
}
},
components: {
BookTable: BookTable
},
mounted() {
// if (this.$store.state.token === undefined || this.$store.state.token === null || this.$store.state.token === '') {
// this.$router.push("/admin")
// }
},
methods: {
selectTab(index) {
this.selectedTab = index;
}
}
}
</script>
<style scoped>
.admin-dashboard{
margin:0 auto;
padding: 15px;
}
button.tab {
background-color: #baa48f;
border:none;
padding: 4px;
cursor: pointer;
}
button.tab.active {
border-top: solid 2px #2f2014;
font-weight: bold;
}
.shown-container {
background-color: #baa48f;
color: #2f2014;
padding:5px
}
</style>
@@ -0,0 +1,159 @@
<template>
<div class="editor">
<div class="column">
<h2>{{editorTitle}}</h2>
<div class="error" v-if="this.error != null"><p v-html="this.error"></p></div>
<input class="savebtn" type="submit" value="Save" />
<router-link class="cancelbtn" :to="{ path: '/' }">Cancel</router-link>
<SingleLineEdit name="title"
label="Title"
v-bind:value="entry.title"
@valueChanged="v => onTitle(v)" />
<SingleLineEdit name="author"
label="Author"
v-bind:value="entry.author"
@valueChanged="v => onAuthor(v)" />
<DateEdit name="yearPublished"
label="Year published"
v-bind:value="entry.yearPublished"
@valueChanged="v => onYear(v)" />
<MultiLineEdit name="bookIndex"
label="Book index"
v-bind:value="entry.bookIndex"
@valueChanged="v => onIndex(v)" />
</div>
</div>
</template>
<script>
import SingleLineEdit from './SingleLineEdit.vue';
import MultiLineEdit from './MultiLineEdit.vue';
import DateEdit from './DateEdit.vue';
export default {
name: "BookEditor",
model: {
prop: 'entry',
event: 'change'
},
props: {
error: "AAAAAA",
is_edit: {
type: Boolean,
default() {
return false;
}
},
editorTitle: {
type: String,
default() {
return 'Title';
}
},
entry: {
id: {
type: String,
default() {
return null;
}
},
title: {
type: String,
default() {
return null;
}
},
author: {
type: String,
default() {
return null;
}
},
yearPublished: {
type: Date,
default() {
return null;
}
},
bookIndex: {
type: String,
default() {
return null;
}
},
}
},
components: {
SingleLineEdit,
MultiLineEdit,
DateEdit
},
methods: {
onIndex(x) {
this.$emit('modelEdit', {
id: this.entry.id,
yearPublished: this.entry.yearPublished,
title: this.entry.title,
author: this.entry.author,
bookIndex: x
});
},
onAuthor(x) {
this.$emit('modelEdit', {
id: this.entry.id,
yearPublished: this.entry.yearPublished,
title: this.entry.title,
author: x,
bookIndex: this.entry.bookIndex
});
},
onTitle(x) {
this.$emit('modelEdit', {
id: this.entry.id,
yearPublished: this.entry.yearPublished,
title: x,
author: this.entry.author,
bookIndex: this.entry.bookIndex
});
},
onYear(x) {
this.$emit('modelEdit', {
id: this.entry.id,
yearPublished: x,
title: this.entry.title,
author: this.entry.author,
bookIndex: this.entry.bookIndex
});
},
},
}
</script>
<style scoped>
.savebtn {
margin: 15px;
font-size: 24px;
color: #2f2014;
background-color: #c3ffa8;
border: none;
}
.cancelbtn {
margin: 6px;
font-size: 15px;
color: #2f2014;
background-color: #ffa8a9;
border: none;
}
.error {
overflow: auto;
max-width: 50%;
max-height: 150px;
color: red;
}
</style>
@@ -0,0 +1,81 @@
<template>
<div class="editor-line">
<div class="label">
<label v-if="this.label != null">{{label}}</label>
</div>
<div class="input">
<input
:name="name"
type="date"
v-model="innerValue"
:disabled="is_disabled" />
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: "DateEdit",
props: {
label: null,
name: null,
value: {
type: Date,
default() {
return new Date();
}
},
is_disabled: {
type: Boolean,
default() {
return false;
}
},
},
data() {
return {
innerValue: new Date()
}
},
watch: {
innerValue: {
handler: function (x) {
this.$emit('valueChanged', x);
},
immediate: false
},
value: {
handler: function(x) {
this.innerValue = x.toISOString().slice(0, 10);
},
immediate: false
}
},
mounted() {
}
});
</script>
<style scoped>
.label, .input {
display: inline-block;
}
.label {
margin-right: 5px;
min-width: 39%;
}
.input {
width: 60%;
}
.input > input {
width: 100%;
}
.editor-line {
margin-top: 15px;
margin-bottom: 10px;
width: 100%;
border-top: dashed 1px #2f2014;
}
</style>
@@ -0,0 +1,90 @@
<template>
<div class="editor-line">
<div class="label">
<label v-if="this.label != null">{{label}}</label>
</div>
<div class="input">
<!-- <textarea :name="name" v-model="message_slug" :disabled="is_disabled" /> -->
<VAceEditor
ref="aceRef"
v-model:value="message_slug"
style="height: 300px"
:options="{ useWorker: false }"
/>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { VAceEditor } from 'vue3-ace-editor';
export default {
name: "MultiLineEdit",
data() {
return {
message_slug: ''
}
},
components: {
VAceEditor: VAceEditor
},
props: {
label: null,
name: null,
value: '',
is_disabled: {
type: Boolean,
default() {
return false;
}
},
},
watch: {
message_slug: {
handler: function (x) {
this.$emit('valueChanged', x);
},
immediate: false
},
value: {
handler: function (x) {
this.message_slug = x;
},
immediate: false
}
},
mounted() {
this.message_slug = this.value;
}
};
</script>
<style scoped>
.label, .input {
display: inline-block;
}
.label {
margin-right: 5px;
min-width: 39%;
vertical-align: top;
}
.input {
width: 60%;
}
.input > textarea {
width: 100%;
}
.editor-line {
margin-top: 15px;
margin-bottom: 10px;
width: 100%;
border-top: dashed 1px #2f2014;
}
</style>
@@ -0,0 +1,82 @@
<template>
<div class="editor-line">
<div class="label">
<label v-if="this.label != null">{{label}}</label>
</div>
<div class="input">
<input
:name="name"
type="text"
v-model="innerValue"
:disabled="is_disabled" />
</div>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: "SingleLineEdit",
props: {
label: null,
name: null,
value: {
type: String,
default() {
return '';
}
},
is_disabled: {
type: Boolean,
default() {
return false;
}
},
},
data() {
return {
innerValue: ''
}
},
watch: {
innerValue: {
handler: function (x) {
this.$emit('valueChanged', x);
},
immediate: false
},
value: {
handler: function(x) {
this.innerValue = x;
},
immediate: false
}
},
mounted() {
this.innerValue = this.value;
}
});
</script>
<style scoped>
.label, .input {
display: inline-block;
}
.label {
margin-right: 5px;
min-width: 39%;
}
.input {
width: 60%;
}
.input > input {
width: 100%;
}
.editor-line {
margin-top: 15px;
margin-bottom: 10px;
width: 100%;
border-top: dashed 1px #2f2014;
}
</style>
+2
View File
@@ -0,0 +1,2 @@
import config from './infrastructure/config.js';
export default config;
+4
View File
@@ -0,0 +1,4 @@
export default {
API_URL: 'http://localhost:5222/api',
DEFAULT_TITLE: 'Books'
}
+88
View File
@@ -0,0 +1,88 @@
import { createApp } from 'vue';
import App from './App.vue';
import { createWebHistory, createRouter } from 'vue-router';
import { createStore } from 'vuex'
import Home from './components/Home.vue';
import BookAdd from './components/admin/BookAdd.vue';
import BookEdit from './components/admin/BookEdit.vue';
import config from './config.js'
import ace from 'ace-builds';
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl);
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url';
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl);
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url'; // For vite
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
let siteName = 'Not rocket science';
const routes = [
{
path: '/',
component: Home,
meta: { title: siteName }
},
{
path: '/add-book',
component: BookAdd,
meta: { title: siteName + " EDIT" }
},
{
path: '/edit-book/:id',
component: BookEdit,
meta: { title: siteName + " EDIT" }
},
];
const store = new createStore({
state: {
token: null,
config: {
API_URL: config.API_URL
}
},
mutations: {
setToken(state, token) {
state.token = token
},
purgeToken(state) {
state.token = null
},
initialiseStore(state) {
var tokenInStorage = localStorage.getItem('token');
if (tokenInStorage) {
state.token = JSON.parse(tokenInStorage);
}
}
},
});
store.subscribe((mutation, state) => {
localStorage.setItem('token', JSON.stringify(state.token));
});
const router = new createRouter({
routes: routes,
history: createWebHistory()
});
router.afterEach((to) => {
document.title = to.meta.title || config.DEFAULT_TITLE;
});
const app = createApp({
extends: App,
beforeCreate() {
this.$store.commit('initialiseStore')
}
})
.use(router)
.use(store)
.mount('#app')