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
+3
View File
@@ -0,0 +1,3 @@
bin/
node_modules/
obj/
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}
+12
View File
@@ -0,0 +1,12 @@
FROM node:18-alpine as install
WORKDIR /app
COPY ./frontend/package*.json ./
RUN ls -la
RUN npm install
FROM install as run
COPY ./frontend .
RUN ["npm", "run", "build"]
ENTRYPOINT ["npm", "run", "production"]
+4
View File
@@ -0,0 +1,4 @@
# frontend
Run:
$ npm run dev
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
@font-face{font-family:Buran;src:local("Buran USSR"),url(/assets/Buran\ USSR-4a5dccb7.ttf) format("truetype")}@font-face{font-family:Mustica;src:local("MusticaPro-SemiBold"),url(/assets/MusticaPro-SemiBold-7bb0a7cb.otf) format("opentype")}html{height:100%;color:#fff;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:#fff}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:#fff;text-decoration:none;margin-left:7px;margin-right:7px;text-shadow:0px 0px 6px #2f2014}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}.addbtn[data-v-3a1e7e07]{font-size:24px}thead[data-v-3a1e7e07]{background-color:#2f2014;color:#fff}tr[data-v-3a1e7e07],td[data-v-3a1e7e07]{border:dotted 1px #2f2014}.admin-dashboard[data-v-e19aee3b]{margin:0 auto;padding:15px}button.tab[data-v-e19aee3b]{background-color:#baa48f;border:none;padding:4px;cursor:pointer}button.tab.active[data-v-e19aee3b]{border-top:solid 2px #2f2014;font-weight:700}.shown-container[data-v-e19aee3b]{background-color:#baa48f;color:#2f2014;padding:5px}h2[data-v-aad8fcac]{color:#2f2014}.list[data-v-aad8fcac]{display:flex;flex-direction:row}.list-container[data-v-aad8fcac]{margin-left:-5px;margin-right:-5px}.list-elem[data-v-aad8fcac]{width:25%;padding:2px 4px 2px 2px}.label[data-v-d6ae499b],.input[data-v-d6ae499b]{display:inline-block}.label[data-v-d6ae499b]{margin-right:5px;min-width:39%}.input[data-v-d6ae499b]{width:60%}.input>input[data-v-d6ae499b]{width:100%}.editor-line[data-v-d6ae499b]{margin-top:15px;margin-bottom:10px;width:100%;border-top:dashed 1px #2f2014}.label[data-v-d1d12cd2],.input[data-v-d1d12cd2]{display:inline-block}.label[data-v-d1d12cd2]{margin-right:5px;min-width:39%;vertical-align:top}.input[data-v-d1d12cd2]{width:60%}.input>textarea[data-v-d1d12cd2]{width:100%}.editor-line[data-v-d1d12cd2]{margin-top:15px;margin-bottom:10px;width:100%;border-top:dashed 1px #2f2014}.label[data-v-b8a707c3],.input[data-v-b8a707c3]{display:inline-block}.label[data-v-b8a707c3]{margin-right:5px;min-width:39%}.input[data-v-b8a707c3]{width:60%}.input>input[data-v-b8a707c3]{width:100%}.editor-line[data-v-b8a707c3]{margin-top:15px;margin-bottom:10px;width:100%;border-top:dashed 1px #2f2014}.savebtn[data-v-ce0e1569]{margin:15px;font-size:24px;color:#2f2014;background-color:#c3ffa8;border:none}.cancelbtn[data-v-ce0e1569]{margin:6px;font-size:15px;color:#2f2014;background-color:#ffa8a9;border:none}.error[data-v-ce0e1569]{overflow:auto;max-width:50%;max-height:150px;color:red}
+9
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDuEEAg7hBAIO4QSWDuEEgg7hBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg7hBAIO4QQiDuEGlg7hBmIO4QQSDuEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg7hBAIO4QQCDuEFQg7hB9IO4Qe6DuEFDg7hBAIO4QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIO4QQCDuEEUg7hBwYO4Qf+DuEH/g7hBtIO4QQ2DuEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIO4QQCDuEEAg7hBb4O4QfyDuUH/g7lB/4O4QfmDuEFgg7hBAIO4QQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACDuEEAg7hBJoO4QdmDuUH/f6tA/3+tQP+DuUH/g7hBzoO4QR2DuEEAAAAAAAAAAAAAAAAAAAAAAAAAAACDuEEAg7hBA4O4QY+DuEH/grZB/2xzOv9ueTr/g7dB/4O4Qf+DuEGAg7hBAIO4QQAAAAAAAAAAAAAAAACDuEEAg7hBAIO4QT6DuEHrhLpB/3mYPv9gTjb/YVE2/3qePv+DuUH/g7hB44O4QTKDuEEAg7hBAAAAAAAAAAAAg7hBAIO4QQuDuEGvg7lB/4GxQP9oZjj/XUc1/11HNf9qbDn/grRB/4O4Qf+DuEGhg7hBB4O4QQAAAAAAg7hBAIO4QQCDuEFbg7hB94O5Qf90izz/Xko1/15JNf5eSTX+X0w1/3aRPf+DuUH/g7hB8oO4QU2DuEEAg7hBAIO4QQCDuEEZg7hByoO5Qf9+qkD/ZFs3/15INf9eSjW/Xko1xl5INf9mYDj/gK5A/4O5Qf+DuEG+g7hBEoO4QQCDuEEAg7hBe4O4Qf6Dt0H/b3w7/15INf9eSTXtX0s1P19LNUxeSTXzXkk1/3GDO/+DuEH/g7hB/IO4QWyDuEEAg7hBKIO4QciDuUHpfKM/52JUNudeSDXqXko1jmFRNgNgTjYHXko1m15INepjWDfnfqg/54O5QeqDuEG+g7hBHoO4QQ6DuEEmhLpBJnWNPCZdRzUmXkk1Jl9KNQ9eSTUAXUc1AF9KNRFeSTUnXkk1JneUPSaEukEmg7hBJoO4QQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP5/AAD8PwAA/D8AAPgfAAD4HwAA8A8AAOAPAADgBwAAwAMAAMADAACAAQAAgAEAAAAAAAABgAAA//8AAA==">
<title>Notrocketscience</title>
<script type="module" crossorigin src="/assets/index-6a80e0e5.js"></script>
<link rel="stylesheet" href="/assets/index-2570ef72.css">
</head>
<body>
<noscript>
<strong>javascript not enabled</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+158
View File
@@ -0,0 +1,158 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>a14c5984-6907-420a-900f-ec6f55275e21</ProjectGuid>
<ProjectHome>.</ProjectHome>
<StartupFile>npm</StartupFile>
<SearchPath>
</SearchPath>
<WorkingDirectory>.</WorkingDirectory>
<OutputPath>.</OutputPath>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
<NodejsPort>5173</NodejsPort>
<StartWebBrowser>True</StartWebBrowser>
<ScriptArguments>run dev</ScriptArguments>
<SaveNodeJsSettingsInProjectFile>False</SaveNodeJsSettingsInProjectFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Content Include="dist\index.html" />
<Content Include="Dockerfile" />
<Content Include="index.html">
<SubType>Code</SubType>
</Content>
<Content Include="package-lock.json" />
<Content Include="favicon.ico" />
<Content Include="src\App.vue" />
<Content Include="src\assets\fonts\Buran USSR.ttf" />
<Content Include="src\assets\fonts\MusticaPro-SemiBold.otf" />
<Content Include="src\assets\fonts\README.txt" />
<Content Include="src\components\About.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\ComicEdit.vue" />
<Content Include="src\components\admin\Dashboard.vue" />
<Content Include="src\components\admin\Admin.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\FileUpload.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\ComicEditor.vue" />
<Content Include="src\components\admin\elements\GameEditor.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\ImageUploader.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\LinkEdit.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\NumberEdit.vue" />
<Content Include="src\components\admin\ComicAdd.vue" />
<Content Include="src\components\admin\GameAdd.vue" />
<Content Include="src\components\admin\GameEdit.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\MultiLineEdit.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\elements\SingleLineEdit.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\admin\ComicTable.vue" />
<Content Include="src\components\elements\CarouselGallery.vue" />
<Content Include="src\components\admin\GameTable.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\elements\games\GameLinks.vue" />
<Content Include="src\components\elements\Greeter.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\Game.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\elements\games\GameDescription.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\elements\games\GamePreview.vue">
<SubType>Code</SubType>
</Content>
<Content Include="src\components\Games.vue" />
<Content Include="src\components\Home.vue" />
<Content Include="package.json" />
<Content Include="README.md" />
</ItemGroup>
<ItemGroup>
<Folder Include="dist\" />
<Folder Include="src\" />
<Folder Include="src\assets\" />
<Folder Include="src\assets\fonts\" />
<Folder Include="src\assets\js\" />
<Folder Include="src\components\" />
<Folder Include="src\components\admin\elements\" />
<Folder Include="src\components\elements\games\" />
<Folder Include="src\components\elements\" />
<Folder Include="src\components\admin\" />
<Folder Include="src\infrastructure\" />
</ItemGroup>
<ItemGroup>
<Content Include="src\config.js" />
<Content Include="src\infrastructure\config.js">
<SubType>Code</SubType>
</Content>
<Content Include="src\main.js" />
</ItemGroup>
<PropertyGroup>
<PostBuildEvent>
npm run build
</PostBuildEvent>
</PropertyGroup>
<Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>0</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:48022/</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://localhost:1337</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
<WebProjectProperties>
<StartPageUrl>
</StartPageUrl>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>False</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>
<SQLDebugging>False</SQLDebugging>
<ExternalProgram>
</ExternalProgram>
<StartExternalURL>
</StartExternalURL>
<StartCmdLineArguments>
</StartCmdLineArguments>
<StartWorkingDirectory>
</StartWorkingDirectory>
<EnableENC>False</EnableENC>
<AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
<NodeExePath>D:\Program Files\nodejs\node.exe</NodeExePath>
</PropertyGroup>
</Project>
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>Notrocketscience</title>
</head>
<body>
<noscript>
<strong>javascript not enabled</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2960
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "3",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"production": "vite preview --host 0.0.0.0"
},
"dependencies": {
"ace-builds": "^1.44.0",
"vue": "^3.2.47",
"vue-router": "^4.1.6",
"vue3-ace-editor": "^2.2.4",
"vue3-carousel": "^0.2.16",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"eslint": "^8.34.0",
"eslint-plugin-vue": "^9.9.0",
"vite": "^4.1.4"
}
}
+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')
+14
View File
@@ -0,0 +1,14 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})