├── .DS_Store
├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── .idea
├── .gitignore
├── StartVue.iml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
└── vcs.xml
├── ClientApp
├── .env
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── orders_example_input.json
│ ├── social_preview.png
│ └── tasks_example_input.json
└── src
│ ├── App.vue
│ ├── assets
│ ├── bootstrap.webp
│ ├── bootstrap_disabled.webp
│ ├── bootstrap_green.webp
│ ├── bootstrap_white.webp
│ ├── css.webp
│ ├── css_disabled.webp
│ ├── css_green.webp
│ ├── css_white.webp
│ ├── tailwind.webp
│ ├── tailwind_disabled.webp
│ ├── tailwind_green.webp
│ ├── tailwind_white.webp
│ ├── vue_logo.webp
│ ├── vuecoon_default.webp
│ ├── vuecoon_error.webp
│ ├── vuecoon_loading.webp
│ └── vuecoon_success.webp
│ ├── components
│ ├── BrowserFrame.vue
│ ├── CodeMirror.vue
│ ├── Editor.vue
│ ├── GenerateSettings.vue
│ ├── GitHubUser.vue
│ ├── ModalPanel.vue
│ ├── NotFound.vue
│ ├── Supporters.vue
│ └── Tab.vue
│ ├── main.js
│ └── utils
│ ├── Helper.js
│ ├── PrettyPrint.js
│ ├── Schema.js
│ ├── Tip.js
│ └── Validate.js
├── LICENSE
├── MobileApp
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│ ├── example_input.json
│ ├── favicon.ico
│ ├── index.html
│ └── social_preview.png
└── src
│ ├── App.vue
│ ├── assets
│ ├── bootstrap.webp
│ ├── bootstrap_green.webp
│ ├── bootstrap_white.webp
│ ├── css.webp
│ ├── css_green.webp
│ ├── css_white.webp
│ ├── html_css_js_logo.webp
│ ├── nuxt_coming_soon.webp
│ ├── nuxt_logo.webp
│ ├── tailwind.webp
│ ├── tailwind_green.webp
│ ├── tailwind_white.webp
│ ├── vue_coming_soon.webp
│ ├── vue_logo.webp
│ ├── vuecoon_default.webp
│ ├── vuecoon_error.webp
│ ├── vuecoon_loading.webp
│ └── vuecoon_success.webp
│ ├── components
│ ├── Alert.vue
│ ├── BrowserFrame.vue
│ ├── BrowserOptions.vue
│ ├── CodeMirror.vue
│ ├── Editor.vue
│ ├── GitHubUser.vue
│ ├── Landing.vue
│ ├── NotFound.vue
│ ├── Settings.vue
│ └── Supporters.vue
│ ├── main.js
│ └── utils
│ ├── Helper.js
│ ├── PrettyPrint.js
│ ├── Schema.js
│ └── Validate.js
├── README.md
├── Server
├── .gitignore
├── .vscode
│ ├── launch.json
│ └── tasks.json
├── ApplicationDbContext.cs
├── Authorization
│ ├── AllowAnonymousAttribute.cs
│ ├── AuthorizeAttribute.cs
│ └── BasicAuthMiddleware.cs
├── Controllers
│ ├── AdminController.cs
│ ├── ClientErrorsController.cs
│ ├── DownloadController.cs
│ ├── FilesController.cs
│ ├── GenerationController.cs
│ └── ShareController.cs
├── Data
│ ├── ActionType.cs
│ └── Frontend.cs
├── Entities
│ ├── ClientError.cs
│ ├── InputData.cs
│ ├── ServerError.cs
│ ├── ShareableLink.cs
│ ├── StatisticRecord.cs
│ ├── User.cs
│ ├── Visit.cs
│ └── Visitor.cs
├── Migrations
│ ├── 20220509123222_InitialCreate.Designer.cs
│ ├── 20220509123222_InitialCreate.cs
│ ├── 20220527143241_ShareableLinkSettings.Designer.cs
│ ├── 20220527143241_ShareableLinkSettings.cs
│ └── ApplicationDbContextModelSnapshot.cs
├── Models
│ ├── AuthenticateModel.cs
│ ├── ChangePasswordModel.cs
│ ├── EventData.cs
│ ├── GenerateRequest.cs
│ ├── GenerateSettings.cs
│ └── VisitorData.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Services
│ ├── ErrorHandlerService.cs
│ ├── GenerationService.cs
│ ├── GeoLocationService.cs
│ ├── InputStatisticService.cs
│ ├── StatisticsService.cs
│ ├── UserService.cs
│ └── VisitorStatisticService.cs
├── Settings
│ ├── ClassSettings.cs
│ └── PropertySettings.cs
├── Startup.cs
├── VueStart.csproj
├── appsettings.Development.json
├── appsettings.json
└── templates
│ ├── bootstrap-index.sbn
│ ├── bootstrap-table-editable.sbn
│ ├── bootstrap-table.sbn
│ ├── tailwind-index.sbn
│ ├── tailwind-table-editable.sbn
│ ├── tailwind-table.sbn
│ ├── vanilla-index.sbn
│ ├── vanilla-table-editable.sbn
│ └── vanilla-table.sbn
└── vs_demo.webp
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/.DS_Store
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Setup .NET
13 | uses: actions/setup-dotnet@v1
14 | with:
15 | dotnet-version: 6.0.x
16 | lfs: true
17 | - name: Use Node.js 16.x
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: 16.x
21 | - name: Checkout LFS objects
22 | run: git lfs pull
23 | - name: Install dependencies (nuget)
24 | run: dotnet restore
25 | working-directory: ./Server
26 | - name: Build Server
27 | run: dotnet publish -c Release
28 | working-directory: ./Server
29 | - name: Archive built server application
30 | uses: actions/upload-artifact@v2
31 | with:
32 | name: server
33 | path: Server/bin/Release/net6.0/publish
34 | - name: Install dependencies (npm, desktop)
35 | run: npm ci
36 | working-directory: ./ClientApp
37 | - name: Build Client (desktop)
38 | run: npm run build
39 | working-directory: ./ClientApp
40 | - name: Archive built client application (desktop)
41 | uses: actions/upload-artifact@v2
42 | with:
43 | name: client
44 | path: ClientApp/dist
45 | - name: Install dependencies (npm, mobile)
46 | run: npm ci
47 | working-directory: ./MobileApp
48 | - name: Build Client (mobile)
49 | run: npm run build
50 | working-directory: ./MobileApp
51 | - name: Archive built client application (mobile)
52 | uses: actions/upload-artifact@v2
53 | with:
54 | name: mobile
55 | path: MobileApp/dist
56 |
57 |
58 | deploy:
59 | needs: build
60 | runs-on: ubuntu-latest
61 | steps:
62 | - name: Trigger webhook
63 | run: curl -s http://vuestart.com:9000/hooks/update-vuestart?token=${{ secrets.WEBHOOK_TOKEN }}
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Generated
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/StartVue.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ClientApp/.env:
--------------------------------------------------------------------------------
1 | VUE_APP_UI=vanilla
--------------------------------------------------------------------------------
/ClientApp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | "node": true
5 | },
6 | extends: [
7 | "plugin:vue/vue3-essential",
8 | "eslint:recommended"
9 | ],
10 | rules: {}
11 | }
--------------------------------------------------------------------------------
/ClientApp/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/ClientApp/README.md:
--------------------------------------------------------------------------------
1 | # Vue Start
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/ClientApp/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/ClientApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-start",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@codemirror/basic-setup": "^0.19.3",
12 | "@codemirror/lang-html": "^0.19.4",
13 | "@codemirror/lang-javascript": "^0.19.7",
14 | "@codemirror/lang-json": "^0.19.2",
15 | "@codemirror/next": "^0.16.0",
16 | "@codemirror/view": "^0.19.48",
17 | "axios": "^0.26.1",
18 | "core-js": "^3.22.1",
19 | "vue": "^3.2.33"
20 | },
21 | "devDependencies": {
22 | "@babel/eslint-parser": "^7.17.0",
23 | "@vue/cli-plugin-babel": "^5.0.4",
24 | "@vue/cli-service": "^5.0.4",
25 | "@vue/compiler-sfc": "^3.2.33",
26 | "eslint": "^8.13.0",
27 | "eslint-plugin-vue": "^8.6.0"
28 | },
29 | "browserslist": [
30 | "> 1%",
31 | "last 2 versions",
32 | "not dead"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/ClientApp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/public/favicon.ico
--------------------------------------------------------------------------------
/ClientApp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue Start
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/ClientApp/public/orders_example_input.json:
--------------------------------------------------------------------------------
1 | {
2 | "customers": [
3 | {
4 | "name": "Microsoft Corporation",
5 | "registrationTime": "2010-05-23T12:00",
6 | "orders": [
7 | {
8 | "date": "2010-05-23T12:00",
9 | "product": "printer paper",
10 | "quantity": 13
11 | },
12 | {
13 | "date": "2010-06-23T12:00",
14 | "product": "printer paper",
15 | "quantity": 24
16 | }
17 | ]
18 | },
19 | {
20 | "name": "Google LLC",
21 | "registrationTime": "2010-07-29T12:00",
22 | "orders": [
23 | {
24 | "date": "2010-07-29T12:00",
25 | "product": "ink cartridge",
26 | "quantity": 3
27 | }
28 | ]
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/ClientApp/public/social_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/public/social_preview.png
--------------------------------------------------------------------------------
/ClientApp/public/tasks_example_input.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": [
3 | {
4 | "title": "Buy Sour Cream",
5 | "done": true,
6 | "due": "2022-04-19T10:00"
7 | },
8 | {
9 | "title": "Chop Onions",
10 | "done": true,
11 | "due": "2022-04-19T12:00"
12 | },
13 | {
14 | "title": "Cut the meat",
15 | "done": false,
16 | "due": "2022-04-19T12:20"
17 | },
18 | {
19 | "title": "Taste the wine",
20 | "done": false,
21 | "due": "2022-04-19T13:00"
22 | },
23 | {
24 | "title": "Cook Goulash",
25 | "done": false,
26 | "due": "2022-04-19T17:00"
27 | },
28 | {
29 | "title": "Start Dishwasher",
30 | "done": false,
31 | "due": "2022-04-19T17:15"
32 | },
33 | {
34 | "title": "Set the table",
35 | "done": false,
36 | "due": "2022-04-19T17:30"
37 | },
38 | {
39 | "title": "Light the candles",
40 | "done": false,
41 | "due": "2022-04-19T17:45"
42 | }
43 | ]
44 | }
--------------------------------------------------------------------------------
/ClientApp/src/assets/bootstrap.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/bootstrap_disabled.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_disabled.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/bootstrap_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_green.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/bootstrap_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/bootstrap_white.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/css.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/css_disabled.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_disabled.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/css_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_green.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/css_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/css_white.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/tailwind.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/tailwind_disabled.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_disabled.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/tailwind_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_green.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/tailwind_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/tailwind_white.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/vue_logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vue_logo.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/vuecoon_default.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_default.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/vuecoon_error.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_error.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/vuecoon_loading.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_loading.webp
--------------------------------------------------------------------------------
/ClientApp/src/assets/vuecoon_success.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/ClientApp/src/assets/vuecoon_success.webp
--------------------------------------------------------------------------------
/ClientApp/src/components/BrowserFrame.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
25 |
31 |
32 |
33 |
34 |
35 |
126 |
127 |
221 |
--------------------------------------------------------------------------------
/ClientApp/src/components/CodeMirror.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
199 |
200 |
--------------------------------------------------------------------------------
/ClientApp/src/components/GenerateSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
Frontend mode
10 |
27 |
Editable
28 |
42 |
Theme color
43 |
44 |
45 |
46 |
47 |
Table settings
48 |
53 |
101 |
102 |
103 |
104 | {{ errorMessage }}
105 |
106 |
107 |
111 |
112 |
113 |
114 |
115 |
172 |
173 |
--------------------------------------------------------------------------------
/ClientApp/src/components/GitHubUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{name}}
5 |
6 |
7 |
8 |
30 |
31 |
--------------------------------------------------------------------------------
/ClientApp/src/components/ModalPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
19 |
20 |
--------------------------------------------------------------------------------
/ClientApp/src/components/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
404
5 |
Page not found
6 |
The page you are looking for does not exist.
7 |
8 |
9 |
10 |
11 |
12 |
20 |
21 |
--------------------------------------------------------------------------------
/ClientApp/src/components/Supporters.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Staring our project on GitHub may seem like a small thing, but it really keeps us motivated. We thank you for your support!
5 |
6 |
7 |
The Team
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Supporters
17 |
31 |
32 |
33 |
34 |
57 |
58 |
--------------------------------------------------------------------------------
/ClientApp/src/components/Tab.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
30 |
31 |
80 |
--------------------------------------------------------------------------------
/ClientApp/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.vue'
2 | import { createApp } from 'vue'
3 | import axios from 'axios';
4 |
5 |
6 | const app = createApp(App);
7 | app.mount('#app')
8 |
9 | app.config.errorHandler = function (err, vm, info) {
10 | axios.post('api/errors', { ...err, info, vm })
11 | };
12 |
13 | window.onerror = function(event, source, lineno, colno, error) {
14 | axios.post('api/errors', { ...error, event, lineno, colno, source })
15 | };
--------------------------------------------------------------------------------
/ClientApp/src/utils/Helper.js:
--------------------------------------------------------------------------------
1 | export function toPascalCase(str) {
2 | return str
3 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
4 | .replace(/\s/g, '')
5 | .replace(/^(.)/, function($1) { return $1.toUpperCase(); });
6 | }
7 |
8 | export function toCamelCase(str) {
9 | return str
10 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
11 | .replace(/\s/g, '')
12 | .replace(/^(.)/, function($1) { return $1.toLowerCase(); });
13 | }
14 |
15 | export function getJsonLength (json){
16 | json = json.replace(/ {2}/g, '');
17 | json = json.replace(/": /g, '":');
18 | json = json.replace(/[\n\t\r]/g, '');
19 | return json.length;
20 | }
21 |
22 | export function getJsonLineNumber(text){
23 | return text.split('\n').length;
24 | }
25 |
26 | export function debounce (func, wait) {
27 | let timeout;
28 | return function executedFunction(...args) {
29 | const later = () => {
30 | clearTimeout(timeout);
31 | func(...args);
32 | };
33 | clearTimeout(timeout);
34 | timeout = setTimeout(later, wait);
35 | };
36 | }
--------------------------------------------------------------------------------
/ClientApp/src/utils/PrettyPrint.js:
--------------------------------------------------------------------------------
1 | import { toCamelCase } from './Helper';
2 |
3 | export function formatJson(json){
4 | json = json.replaceAll(/\s/g,'');
5 | json = json.replaceAll('{','{\n');
6 | json = json.replaceAll('}','\n}');
7 | json = json.replaceAll('[','[\n');
8 | json = json.replaceAll(']','\n]');
9 | json = json.replaceAll(',',',\n');
10 | json = json.replaceAll(':',': ');
11 | return json;
12 | }
13 | function indentLines(lines){
14 | let tabCount = 0;
15 | for(let i = 0; i < lines.length; i++){
16 | if(lines[i].includes('{') || lines[i].includes('[')){
17 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
18 | tabCount++;
19 | }else if(lines[i].includes('}') || lines[i].includes(']')){
20 | --tabCount;
21 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
22 | }else{
23 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
24 | }
25 | }
26 | return lines;
27 | }
28 | function replaceToKey(strings, lines){
29 | strings.forEach(key => {
30 | const name = key.replace(/"([^"]*)":/g, '$1')
31 | for(let i = 0; i < lines.length; i++){
32 | if(lines[i].includes('key')){
33 | lines[i] = lines[i].replace('key', `"${toCamelCase(name)}": `);
34 | break;
35 | }
36 | }
37 | });
38 | return lines;
39 | }
40 | function replaceToString(strings, lines){
41 | strings.forEach(string => {
42 | for(let i = 0; i < lines.length; i++){
43 | if(lines[i].includes('""')){
44 | lines[i] = lines[i].replace('""', string);
45 | break;
46 | }
47 | }
48 | });
49 | return lines;
50 | }
51 | export function prettyPrint(json) {
52 | json = json.replaceAll('\r','');
53 | json = json.replaceAll('""','""');
54 | const keys = json.match(/"([^"]*)":/g);
55 | json = json.replace(/"([^"]*)":/g, 'key');
56 | const strings = json.match(/"[^"]*"/g);
57 | json = json.replace(/"[^"]*"/g, '""');
58 | json = formatJson(json);
59 | let lines = indentLines(json.split('\n'));
60 | if (keys) {
61 | lines = replaceToKey(keys, lines);
62 | }
63 | if (strings) {
64 | lines = replaceToString(strings, lines);
65 | }
66 | lines.forEach((line, idx) =>{
67 | if((/^(\t)+$/g).test(line) || line === ''){
68 | lines.splice(idx, 1);
69 | }
70 | })
71 | lines = lines.join('\n');
72 | lines = lines.replaceAll('""','""');
73 | return lines;
74 | }
75 |
--------------------------------------------------------------------------------
/ClientApp/src/utils/Schema.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function mergeDeep(obj1, obj2) {
4 | const isObject = (obj) => obj && typeof obj === 'object';
5 |
6 | if (!isObject(obj1) || !isObject(obj2)) {
7 | return obj1 || obj2;
8 | }
9 |
10 | let result = {};
11 |
12 | mergeArrays(Object.keys(obj2), Object.keys(obj1)).forEach(key => {
13 | const value1 = obj1[key];
14 | const value2 = obj2[key];
15 | if (Array.isArray(value1) && Array.isArray(value2)) {
16 | let mergedArray = mergeArrays(value1, value2);
17 | result[key] = mergedArray;
18 | } else if (isObject(value1) && isObject(value2)) {
19 | result[key] = mergeDeep(value1, value2);
20 | } else if (value1) {
21 | result[key] = value1;
22 | } else {
23 | result[key] = value2;
24 | }
25 | });
26 |
27 | return result;
28 | }
29 |
30 | function mergeArrays(array1, array2) {
31 | let mergedArray = [...array1];
32 | array2.forEach(val => {
33 | if (!mergedArray.includes(val)) {
34 | mergedArray.push(val);
35 | }
36 | });
37 | return mergedArray;
38 | }
39 |
40 | function getArraySchema(val) {
41 | let r = {type: [], properties: {}}
42 | val.forEach( function (v) {
43 | const type = getType(v);
44 | if (!r.type.includes(type)) {
45 | r.type.push(type);
46 | }
47 | r.properties = mergeDeep(r.properties, getSchema(v).properties)
48 | })
49 | return r;
50 | }
51 |
52 |
53 | function getProperties(j) {
54 | if (!j) {
55 | return null;
56 | }
57 | let r = {};
58 | let k = Object.keys(j);
59 | k.forEach(function(name) {
60 | r[name] = getSchema(j[name]);
61 | })
62 | return r;
63 | }
64 |
65 | function getType(val) {
66 |
67 | if (Array.isArray(val)) {
68 | return 'array';
69 | }
70 |
71 | if (typeof val == 'object') {
72 | return 'object';
73 | }
74 |
75 | if (typeof val === 'number') {
76 | if (Number.isInteger(val)) {
77 | return 'integer';
78 | }
79 | return 'float';
80 | }
81 |
82 | if (typeof val === 'string') {
83 | let regexExp = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?)$/gi;
84 | if (regexExp.test(val))
85 | return 'datetime'
86 | regexExp = /^(http|https):\/\/.*/gi;
87 | if (regexExp.test(val))
88 | return 'link'
89 | }
90 |
91 | return typeof val;
92 | }
93 |
94 | export function getSchema(val) {
95 | let type = getType(val);
96 |
97 | if (type === 'array') {
98 | return {type: [type], items: getArraySchema(val)};
99 | }
100 |
101 | if (type === 'object') {
102 | return {type: [type], properties: getProperties(val)};
103 | }
104 |
105 | return { type: [type] };
106 | }
107 |
108 |
--------------------------------------------------------------------------------
/ClientApp/src/utils/Tip.js:
--------------------------------------------------------------------------------
1 |
2 | export default class Tip {
3 | constructor() {
4 | this.tipIdx = parseInt(localStorage.getItem('tipIdx')) || 0;
5 | this.tips = [
6 | 'Try to edit the JSON data, and see the changes in the application.',
7 | 'If you make structural changes, the application is regenerated.',
8 | 'Try out multiple layouts with the button in the bottom right corner.',
9 | 'When you are done, click the download button.'
10 | ];
11 | }
12 |
13 | hideTips() {
14 | this.tipIdx = this.tips.length;
15 | localStorage.setItem('tipIdx', this.tipIdx.toString());
16 | }
17 |
18 | modified() {
19 | if (this.tipIdx === 0) {
20 | this.tipIdx = 1;
21 | localStorage.setItem('tipIdx', this.tipIdx.toString());
22 | return true
23 | }
24 | return false
25 | }
26 | generated() {
27 | if (this.tipIdx === 1) {
28 | this.tipIdx = 2;
29 | localStorage.setItem('tipIdx', this.tipIdx.toString());
30 | return true
31 | }
32 | return false
33 | }
34 | typeChanged() {
35 | if (this.tipIdx === 2) {
36 | this.tipIdx = 3;
37 | localStorage.setItem('tipIdx', this.tipIdx.toString());
38 | return true
39 | }
40 | return false
41 | }
42 | downloaded() {
43 | if (this.tipIdx === 3) {
44 | this.tipIdx = 4;
45 | localStorage.setItem('tipIdx', this.tipIdx.toString());
46 | return true
47 | }
48 | return false
49 | }
50 | getTip() {
51 | let msg = this.tips[this.tipIdx];
52 | if (this.tipIdx === 4) {
53 | this.tipIdx = 5;
54 | localStorage.setItem('tipIdx', this.tipIdx.toString());
55 | }
56 | return msg;
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/ClientApp/src/utils/Validate.js:
--------------------------------------------------------------------------------
1 |
2 | function getFrom(str, position, line){
3 | if(!line){
4 | return parseInt(position);
5 | } else {
6 | let charCount = 0;
7 | const strArray = str.split('\n');
8 | for(let i = 0; i < line-1; i++){
9 | charCount += strArray[i].length+1;
10 | }
11 | return charCount + parseInt(position) - 1;
12 | }
13 | }
14 |
15 | function getErrorPosition(str, err) {
16 | let userAgent = navigator.userAgent;
17 | let from = -1;
18 | let to = -1;
19 | let match = err.message.match(/\d+/g);
20 | if (match === null || match.length === 0)
21 | return { from: -1, to: -1 };
22 | if(userAgent.match(/firefox|fxios/i)){
23 | if (match.length === 1)
24 | return { from: -1, to: -1 };
25 | from = getFrom(str, match[1], match[0]);
26 | }else if(userAgent.match(/opr\//i)){
27 | from = getFrom(str, match[0]);
28 | } else if(userAgent.match(/edg/i)){
29 | from = getFrom(str, match[0]);
30 | }else{
31 | from = getFrom(str, match[0]); //chrome
32 | }
33 | if (from > 0) {
34 | let charCount = 0;
35 | const strArray = str.split('\n');
36 | for(let i = 0; i < strArray.length; i++){
37 | charCount += strArray[i].length+1;
38 | if(charCount >= from){
39 | to = charCount-1;
40 | break;
41 | }
42 | }
43 | }
44 | return { from, to };
45 | }
46 |
47 | export function validateJson(text) {
48 | try {
49 | JSON.parse(text);
50 | return {error: false, message: ''};
51 | } catch (err) {
52 | const positions = getErrorPosition(text, err);
53 | return {error: true, from: positions.from, to: positions.to, message: err.message};
54 | }
55 | }
--------------------------------------------------------------------------------
/MobileApp/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | "node": true
5 | },
6 | extends: [
7 | "plugin:vue/vue3-essential",
8 | "eslint:recommended"
9 | ],
10 | rules: {}
11 | }
--------------------------------------------------------------------------------
/MobileApp/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/MobileApp/README.md:
--------------------------------------------------------------------------------
1 | # Vue Start
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/MobileApp/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/MobileApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-start",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@codemirror/basic-setup": "^0.19.3",
12 | "@codemirror/lang-html": "^0.19.4",
13 | "@codemirror/lang-json": "^0.19.2",
14 | "@codemirror/next": "^0.16.0",
15 | "@codemirror/view": "^0.19.48",
16 | "axios": "^0.26.1",
17 | "core-js": "^3.22.1",
18 | "vue": "^3.2.33"
19 | },
20 | "devDependencies": {
21 | "@babel/eslint-parser": "^7.17.0",
22 | "@vue/cli-plugin-babel": "^5.0.4",
23 | "@vue/cli-service": "^5.0.4",
24 | "@vue/compiler-sfc": "^3.2.33",
25 | "eslint": "^8.13.0",
26 | "eslint-plugin-vue": "^8.6.0"
27 | },
28 | "browserslist": [
29 | "> 1%",
30 | "last 2 versions",
31 | "not dead"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/MobileApp/public/example_input.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": [
3 | {
4 | "title": "Buy Sour Cream",
5 | "done": true,
6 | "due": "2022-04-19T10:00"
7 | },
8 | {
9 | "title": "Cook Goulash",
10 | "done": true,
11 | "due": "2022-04-19T12:00"
12 | },
13 | {
14 | "title": "Start Dishwasher",
15 | "done": false,
16 | "due": "2022-04-19T16:00"
17 | }
18 | ]
19 | }
--------------------------------------------------------------------------------
/MobileApp/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/public/favicon.ico
--------------------------------------------------------------------------------
/MobileApp/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue Start
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/MobileApp/public/social_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/public/social_preview.png
--------------------------------------------------------------------------------
/MobileApp/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Star this project on GitHub!
13 |
14 |
15 |
Staring our project on GitHub may seem like a small thing, but it really keeps us motivated. We thank you for your support!
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
119 |
120 |
193 |
--------------------------------------------------------------------------------
/MobileApp/src/assets/bootstrap.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/bootstrap_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap_green.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/bootstrap_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/bootstrap_white.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/css.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/css_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css_green.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/css_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/css_white.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/html_css_js_logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/html_css_js_logo.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/nuxt_coming_soon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/nuxt_coming_soon.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/nuxt_logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/nuxt_logo.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/tailwind.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/tailwind_green.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind_green.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/tailwind_white.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/tailwind_white.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vue_coming_soon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vue_coming_soon.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vue_logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vue_logo.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vuecoon_default.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_default.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vuecoon_error.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_error.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vuecoon_loading.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_loading.webp
--------------------------------------------------------------------------------
/MobileApp/src/assets/vuecoon_success.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/MobileApp/src/assets/vuecoon_success.webp
--------------------------------------------------------------------------------
/MobileApp/src/components/Alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ lastErrorMessage }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
31 |
32 |
--------------------------------------------------------------------------------
/MobileApp/src/components/BrowserFrame.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
100 |
101 |
106 |
--------------------------------------------------------------------------------
/MobileApp/src/components/BrowserOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
37 |
38 |
88 |
--------------------------------------------------------------------------------
/MobileApp/src/components/CodeMirror.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
201 |
202 |
--------------------------------------------------------------------------------
/MobileApp/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Generate
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
160 |
161 |
209 |
--------------------------------------------------------------------------------
/MobileApp/src/components/GitHubUser.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{name}}
5 |
6 |
7 |
8 |
30 |
31 |
--------------------------------------------------------------------------------
/MobileApp/src/components/Landing.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Vue Start!
4 |
![Vuecoon]()
5 |
6 |
7 |
8 |
9 | Turn any JSON into Vue 3 tables! Create well coded Vue 3 tables with Bootstrap, Tailwind CSS or vanilla CSS.
10 |
11 |
For the full feature set please visit this page from a desktop browser.
12 |
13 |
14 |
15 |
16 |
25 |
26 |
--------------------------------------------------------------------------------
/MobileApp/src/components/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
404
5 |
Page not found
6 |
The page you are looking for does not exist.
7 |
8 |
9 |
10 |
11 |
12 |
20 |
21 |
--------------------------------------------------------------------------------
/MobileApp/src/components/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
9 |
10 |
Frontend mode
11 |
31 |
32 |
Editable
33 |
47 |
48 |
Theme color
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
96 |
97 |
--------------------------------------------------------------------------------
/MobileApp/src/components/Supporters.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
The Team
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
Supporters
16 |
32 |
33 |
34 |
35 |
36 |
64 |
65 |
--------------------------------------------------------------------------------
/MobileApp/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.vue'
2 | import { createApp } from 'vue'
3 | import axios from 'axios';
4 |
5 |
6 | const app = createApp(App);
7 | app.mount('#app')
8 |
9 | app.config.errorHandler = function (err, vm, info) {
10 | axios.post('api/errors', { ...err, info, vm })
11 | };
12 |
13 | window.onerror = function(event, source, lineno, colno, error) {
14 | axios.post('api/errors', { ...error, event, lineno, colno, source })
15 | };
--------------------------------------------------------------------------------
/MobileApp/src/utils/Helper.js:
--------------------------------------------------------------------------------
1 | export function toPascalCase(str) {
2 | return str
3 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
4 | .replace(/\s/g, '')
5 | .replace(/^(.)/, function($1) { return $1.toUpperCase(); });
6 | }
7 |
8 | export function toCamelCase(str) {
9 | return str
10 | .replace(/\s(.)/g, function($1) { return $1.toUpperCase(); })
11 | .replace(/\s/g, '')
12 | .replace(/^(.)/, function($1) { return $1.toLowerCase(); });
13 | }
14 |
15 | export function getJsonLength (json){
16 | json = json.replace(/ {2}/g, '');
17 | json = json.replace(/": /g, '":');
18 | json = json.replace(/[\n\t\r]/g, '');
19 | return json.length;
20 | }
21 |
22 | export function getJsonLineNumber(text){
23 | return text.split('\n').length;
24 | }
25 |
26 | export function debounce (func, wait) {
27 | let timeout;
28 | return function executedFunction(...args) {
29 | const later = () => {
30 | clearTimeout(timeout);
31 | func(...args);
32 | };
33 | clearTimeout(timeout);
34 | timeout = setTimeout(later, wait);
35 | };
36 | }
--------------------------------------------------------------------------------
/MobileApp/src/utils/PrettyPrint.js:
--------------------------------------------------------------------------------
1 | import { toCamelCase } from './Helper';
2 |
3 | export function formatJson(json){
4 | json = json.replaceAll(/\s/g,'');
5 | json = json.replaceAll('{','{\n');
6 | json = json.replaceAll('}','\n}');
7 | json = json.replaceAll('[','[\n');
8 | json = json.replaceAll(']','\n]');
9 | json = json.replaceAll(',',',\n');
10 | json = json.replaceAll(':',': ');
11 | return json;
12 | }
13 | function indentLines(lines){
14 | let tabCount = 0;
15 | for(let i = 0; i < lines.length; i++){
16 | if(lines[i].includes('{') || lines[i].includes('[')){
17 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
18 | tabCount++;
19 | }else if(lines[i].includes('}') || lines[i].includes(']')){
20 | --tabCount;
21 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
22 | }else{
23 | lines[i] = '\t'.repeat(tabCount).concat(lines[i].trim());
24 | }
25 | }
26 | return lines;
27 | }
28 | function replaceToKey(strings, lines){
29 | strings.forEach(key => {
30 | const name = key.replace(/"([^"]*)":/g, '$1')
31 | for(let i = 0; i < lines.length; i++){
32 | if(lines[i].includes('key')){
33 | lines[i] = lines[i].replace('key', `"${toCamelCase(name)}": `);
34 | break;
35 | }
36 | }
37 | });
38 | return lines;
39 | }
40 | function replaceToString(strings, lines){
41 | strings.forEach(string => {
42 | for(let i = 0; i < lines.length; i++){
43 | if(lines[i].includes('""')){
44 | lines[i] = lines[i].replace('""', string);
45 | break;
46 | }
47 | }
48 | });
49 | return lines;
50 | }
51 | export function prettyPrint(json) {
52 | json = json.replaceAll('\r','');
53 | json = json.replaceAll('""','""');
54 | const keys = json.match(/"([^"]*)":/g);
55 | json = json.replace(/"([^"]*)":/g, 'key');
56 | const strings = json.match(/"[^"]*"/g);
57 | json = json.replace(/"[^"]*"/g, '""');
58 | json = formatJson(json);
59 | let lines = indentLines(json.split('\n'));
60 | if (keys) {
61 | lines = replaceToKey(keys, lines);
62 | }
63 | if (strings) {
64 | lines = replaceToString(strings, lines);
65 | }
66 | lines.forEach((line, idx) =>{
67 | if((/^(\t)+$/g).test(line) || line === ''){
68 | lines.splice(idx, 1);
69 | }
70 | })
71 | lines = lines.join('\n');
72 | lines = lines.replaceAll('""','""');
73 | return lines;
74 | }
75 |
--------------------------------------------------------------------------------
/MobileApp/src/utils/Schema.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function mergeDeep(obj1, obj2) {
4 | const isObject = (obj) => obj && typeof obj === 'object';
5 |
6 | if (!isObject(obj1) || !isObject(obj2)) {
7 | return obj1 || obj2;
8 | }
9 |
10 | let result = {};
11 |
12 | mergeArrays(Object.keys(obj2), Object.keys(obj1)).forEach(key => {
13 | const value1 = obj1[key];
14 | const value2 = obj2[key];
15 | if (Array.isArray(value1) && Array.isArray(value2)) {
16 | let mergedArray = mergeArrays(value1, value2);
17 | result[key] = mergedArray;
18 | } else if (isObject(value1) && isObject(value2)) {
19 | result[key] = mergeDeep(value1, value2);
20 | } else if (value1) {
21 | result[key] = value1;
22 | } else {
23 | result[key] = value2;
24 | }
25 | });
26 |
27 | return result;
28 | }
29 |
30 | function mergeArrays(array1, array2) {
31 | let mergedArray = [...array1];
32 | array2.forEach(val => {
33 | if (!mergedArray.includes(val)) {
34 | mergedArray.push(val);
35 | }
36 | });
37 | return mergedArray;
38 | }
39 |
40 | function getArraySchema(val) {
41 | let r = {type: [], properties: {}}
42 | val.forEach( function (v) {
43 | const type = getType(v);
44 | if (!r.type.includes(type)) {
45 | r.type.push(type);
46 | }
47 | r.properties = mergeDeep(r.properties, getSchema(v).properties)
48 | })
49 | return r;
50 | }
51 |
52 |
53 | function getProperties(j) {
54 | if (!j) {
55 | return null;
56 | }
57 | let r = {};
58 | let k = Object.keys(j);
59 | k.forEach(function(name) {
60 | r[name] = getSchema(j[name]);
61 | })
62 | return r;
63 | }
64 |
65 | function getType(val) {
66 |
67 | if (Array.isArray(val)) {
68 | return 'array';
69 | }
70 |
71 | if (typeof val == 'object') {
72 | return 'object';
73 | }
74 |
75 | if (typeof val === 'number') {
76 | if (Number.isInteger(val)) {
77 | return 'integer';
78 | }
79 | return 'float';
80 | }
81 |
82 | if (typeof val === 'string') {
83 | const regexExp = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})$/gi;
84 | if (regexExp.test(val))
85 | return 'datetime'
86 | }
87 |
88 | return typeof val;
89 | }
90 |
91 | export function getSchema(val) {
92 | let type = getType(val);
93 |
94 | if (type === 'array') {
95 | return {type: [type], items: getArraySchema(val)};
96 | }
97 |
98 | if (type === 'object') {
99 | return {type: [type], properties: getProperties(val)};
100 | }
101 |
102 | return { type: [type] };
103 | }
104 |
105 |
--------------------------------------------------------------------------------
/MobileApp/src/utils/Validate.js:
--------------------------------------------------------------------------------
1 |
2 | function getFrom(str, position, line){
3 | if(!line){
4 | return parseInt(position);
5 | } else {
6 | let charCount = 0;
7 | const strArray = str.split('\n');
8 | for(let i = 0; i < line-1; i++){
9 | charCount += strArray[i].length+1;
10 | }
11 | return charCount + parseInt(position) - 1;
12 | }
13 | }
14 |
15 | function getErrorPosition(str, err) {
16 | let userAgent = navigator.userAgent;
17 | let from = -1;
18 | let to = -1;
19 | let match = err.message.match(/\d+/g);
20 | if (match === null || match.length === 0)
21 | return { from: -1, to: -1 };
22 | if(userAgent.match(/firefox|fxios/i)){
23 | if (match.length === 1)
24 | return { from: -1, to: -1 };
25 | from = getFrom(str, match[1], match[0]);
26 | }else if(userAgent.match(/opr\//i)){
27 | from = getFrom(str, match[0]);
28 | } else if(userAgent.match(/edg/i)){
29 | from = getFrom(str, match[0]);
30 | }else{
31 | from = getFrom(str, match[0]); //chrome
32 | }
33 | if (from > 0) {
34 | let charCount = 0;
35 | const strArray = str.split('\n');
36 | for(let i = 0; i < strArray.length; i++){
37 | charCount += strArray[i].length+1;
38 | if(charCount >= from){
39 | to = charCount-1;
40 | break;
41 | }
42 | }
43 | }
44 | return { from, to };
45 | }
46 |
47 | export function validateJson(text) {
48 | try {
49 | const obj = JSON.parse(text);
50 | if (typeof obj !== 'object' || Array.isArray(obj))
51 | return {error: true, message: 'The root elements must be an object!'};
52 | let idx = 0;
53 | for (const prop in obj) {
54 | if (typeof obj[prop] !== 'object')
55 | return {error: true, message: 'The root elements must only have a single property with object or array type!'};
56 | idx += 1;
57 | }
58 | return {error: false, message: ''};
59 | } catch (err) {
60 | const positions = getErrorPosition(text, err);
61 | return {error: true, from: positions.from, to: positions.to, message: err.message};
62 | }
63 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Vue Start
6 |
7 |
8 | Turn any JSON into Vue 3 tables!
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ## Introduction
23 |
24 | Create well coded Vue 3 tables with Bootstrap, Tailwind CSS or vanilla CSS. Try it online at [vuestart.com](https://vuestart.com)!
25 |
26 | **Turn this:**
27 | ```js
28 | {
29 | "customers": [
30 | {
31 | "name": "Microsoft Corporation",
32 | "registrationTime": "2010-05-23T12:00",
33 | "orders": [
34 | {
35 | "date": "2010-05-23T12:00",
36 | "product": "printer paper",
37 | "quantity": 13
38 | },
39 | {
40 | "date": "2010-06-23T12:00",
41 | "product": "printer paper",
42 | "quantity": 24
43 | }
44 | ]
45 | },
46 | {
47 | "name": "Google LLC",
48 | "registrationTime": "2010-07-29T12:00",
49 | "orders": [
50 | {
51 | "date": "2010-07-29T12:00",
52 | "product": "ink cartridge",
53 | "quantity": 3
54 | }
55 | ]
56 | }
57 | ]
58 | }
59 | ```
60 | **Into this:**
61 |
62 |
63 |
64 | ### Features
65 |
66 | - Pagination
67 | - Sortable columns
68 | - Navigation for nested data structures
69 | - Easily theme-able
70 |
71 |
72 | ## Licencing
73 | Please note that the licencing of the Vue Start project and the licencing of the code generated by the Vue Start project is different.
74 |
75 | ### Generated Code
76 |
77 |
78 |
79 | The code generated by the Vue Start project is [unlicenced](https://unlicense.org). This means that you may use the generated code without restrictions in any project under any open source or comercial licence.
80 |
81 | ### Vue Start
82 |
83 |
84 |
85 | The Vue Start project itself is available under the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html). If you would like to get a comercial licence, please send an e-mail to [gabor.angyal@codesharp.hu](mailto://gabor.angyal@codesharp.hu).
86 |
--------------------------------------------------------------------------------
/Server/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio LightSwitch build output
300 | **/*.HTMLClient/GeneratedArtifacts
301 | **/*.DesktopClient/GeneratedArtifacts
302 | **/*.DesktopClient/ModelManifest.xml
303 | **/*.Server/GeneratedArtifacts
304 | **/*.Server/ModelManifest.xml
305 | _Pvt_Extensions
306 |
307 | # Paket dependency manager
308 | .paket/paket.exe
309 | paket-files/
310 |
311 | # FAKE - F# Make
312 | .fake/
313 |
314 | # CodeRush personal settings
315 | .cr/personal
316 |
317 | # Python Tools for Visual Studio (PTVS)
318 | __pycache__/
319 | *.pyc
320 |
321 | # Cake - Uncomment if you are using it
322 | # tools/**
323 | # !tools/packages.config
324 |
325 | # Tabs Studio
326 | *.tss
327 |
328 | # Telerik's JustMock configuration file
329 | *.jmconfig
330 |
331 | # BizTalk build output
332 | *.btp.cs
333 | *.btm.cs
334 | *.odx.cs
335 | *.xsd.cs
336 |
337 | # OpenCover UI analysis results
338 | OpenCover/
339 |
340 | # Azure Stream Analytics local run output
341 | ASALocalRun/
342 |
343 | # MSBuild Binary and Structured Log
344 | *.binlog
345 |
346 | # NVidia Nsight GPU debugger configuration file
347 | *.nvuser
348 |
349 | # MFractors (Xamarin productivity tool) working folder
350 | .mfractor/
351 |
352 | # Local History for Visual Studio
353 | .localhistory/
354 |
355 | # BeatPulse healthcheck temp database
356 | healthchecksdb
357 |
358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
359 | MigrationBackup/
360 |
361 | # Ionide (cross platform F# VS Code tools) working folder
362 | .ionide/
363 |
364 | # Fody - auto-generated XML schema
365 | FodyWeavers.xsd
366 |
367 | ##
368 | ## Visual studio for Mac
369 | ##
370 |
371 |
372 | # globs
373 | Makefile.in
374 | *.userprefs
375 | *.usertasks
376 | config.make
377 | config.status
378 | aclocal.m4
379 | install-sh
380 | autom4te.cache/
381 | *.tar.gz
382 | tarballs/
383 | test-results/
384 |
385 | # Mac bundle stuff
386 | *.dmg
387 | *.app
388 |
389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
390 | # General
391 | .DS_Store
392 | .AppleDouble
393 | .LSOverride
394 |
395 | # Icon must end with two \r
396 | Icon
397 |
398 |
399 | # Thumbnails
400 | ._*
401 |
402 | # Files that might appear in the root of a volume
403 | .DocumentRevisions-V100
404 | .fseventsd
405 | .Spotlight-V100
406 | .TemporaryItems
407 | .Trashes
408 | .VolumeIcon.icns
409 | .com.apple.timemachine.donotpresent
410 |
411 | # Directories potentially created on remote AFP share
412 | .AppleDB
413 | .AppleDesktop
414 | Network Trash Folder
415 | Temporary Items
416 | .apdisk
417 |
418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
419 | # Windows thumbnail cache files
420 | Thumbs.db
421 | ehthumbs.db
422 | ehthumbs_vista.db
423 |
424 | # Dump file
425 | *.stackdump
426 |
427 | # Folder config file
428 | [Dd]esktop.ini
429 |
430 | # Recycle Bin used on file shares
431 | $RECYCLE.BIN/
432 |
433 | # Windows Installer files
434 | *.cab
435 | *.msi
436 | *.msix
437 | *.msm
438 | *.msp
439 |
440 | # Windows shortcuts
441 | *.lnk
442 |
443 | # JetBrains Rider
444 | .idea/
445 | *.sln.iml
446 |
447 | ##
448 | ## Visual Studio Code
449 | ##
450 | .vscode/*
451 | !.vscode/settings.json
452 | !.vscode/tasks.json
453 | !.vscode/launch.json
454 | !.vscode/extensions.json
455 |
--------------------------------------------------------------------------------
/Server/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "program": "${workspaceFolder}/bin/Debug/net6.0/VueStart.dll",
13 | "args": [],
14 | "cwd": "${workspaceFolder}",
15 | "stopAtEntry": false,
16 | "serverReadyAction": {
17 | "action": "openExternally",
18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
19 | },
20 | "env": {
21 | "ASPNETCORE_ENVIRONMENT": "Development"
22 | },
23 | "sourceFileMap": {
24 | "/Views": "${workspaceFolder}/Views"
25 | }
26 | },
27 | {
28 | "name": ".NET Core Attach",
29 | "type": "coreclr",
30 | "request": "attach"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/Server/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/VueStart.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/VueStart.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "${workspaceFolder}/VueStart.csproj",
36 | "/property:GenerateFullPaths=true",
37 | "/consoleloggerparameters:NoSummary"
38 | ],
39 | "problemMatcher": "$msCompile"
40 | }
41 | ]
42 | }
--------------------------------------------------------------------------------
/Server/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using Microsoft.AspNetCore.Identity;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.Configuration;
6 |
7 | namespace VueStart;
8 |
9 | public class ApplicationDbContext : DbContext
10 | {
11 | public DbSet InputData { get; set; }
12 | public DbSet StatisticRecords { get; set; }
13 | public DbSet ServerErrors { get; set; }
14 | public DbSet ClientErrors { get; set; }
15 | public DbSet Visitors { get; set; }
16 | public DbSet Visits { get; set; }
17 | public DbSet Users { get; set; }
18 | public DbSet ShareableLinks { get; set; }
19 |
20 | public ApplicationDbContext(DbContextOptions options) : base(options)
21 | {
22 | }
23 |
24 | protected override void OnModelCreating(ModelBuilder modelBuilder)
25 | {
26 | modelBuilder.Entity()
27 | .Property(b => b.Hash)
28 | .IsRequired();
29 | modelBuilder.Entity()
30 | .Property(b => b.Data)
31 | .IsRequired();
32 | modelBuilder.Entity()
33 | .Property(b => b.Token)
34 | .IsRequired();
35 | modelBuilder.Entity()
36 | .HasMany(v => v.Visits);
37 | modelBuilder.Entity()
38 | .HasMany(v => v.StatisticRecords);
39 | modelBuilder.Entity()
40 | .HasOne(v => v.InputData);
41 |
42 | var passwordHasher = new PasswordHasher();
43 | var admin = new User {
44 | Id = 1,
45 | Username = "admin"
46 | };
47 | admin.PasswordHash = passwordHasher.HashPassword(admin, "password");
48 | modelBuilder.Entity().HasData(admin);
49 | }
50 | }
--------------------------------------------------------------------------------
/Server/Authorization/AllowAnonymousAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace VueStart.Authorization;
4 |
5 | [AttributeUsage(AttributeTargets.Method)]
6 | public class AllowAnonymousAttribute : Attribute
7 | { }
--------------------------------------------------------------------------------
/Server/Authorization/AuthorizeAttribute.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart.Authorization;
2 |
3 | using System;
4 | using System.Linq;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Microsoft.AspNetCore.Mvc.Filters;
8 |
9 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
10 | public class AuthorizeAttribute : Attribute, IAuthorizationFilter
11 | {
12 | public void OnAuthorization(AuthorizationFilterContext context)
13 | {
14 | // skip authorization if action is decorated with [AllowAnonymous] attribute
15 | var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any();
16 | if (allowAnonymous)
17 | return;
18 |
19 | var user = (User)context.HttpContext.Items["User"];
20 | if (user == null)
21 | {
22 | // not logged in - return 401 unauthorized
23 | context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
24 |
25 | // set 'WWW-Authenticate' header to trigger login popup in browsers
26 | context.HttpContext.Response.Headers["WWW-Authenticate"] = "Basic realm=\"\", charset=\"UTF-8\"";
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/Server/Authorization/BasicAuthMiddleware.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart.Authorization;
2 |
3 | using System;
4 | using System.Net.Http.Headers;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Http;
8 | using VueStart.Services;
9 |
10 | public class BasicAuthMiddleware
11 | {
12 | private readonly RequestDelegate _next;
13 |
14 | public BasicAuthMiddleware(RequestDelegate next)
15 | {
16 | _next = next;
17 | }
18 |
19 | public async Task Invoke(HttpContext context, UserService userService)
20 | {
21 | try
22 | {
23 | var authHeader = AuthenticationHeaderValue.Parse(context.Request.Headers["Authorization"]);
24 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
25 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':', 2);
26 | var username = credentials[0];
27 | var password = credentials[1];
28 |
29 | // authenticate credentials with user service and attach user to http context
30 | context.Items["User"] = userService.Authenticate(username, password);
31 | }
32 | catch
33 | {
34 | // do nothing if invalid auth header
35 | // user is not attached to context so request won't have access to secure routes
36 | }
37 |
38 | await _next(context);
39 | }
40 | }
--------------------------------------------------------------------------------
/Server/Controllers/AdminController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Mvc;
4 | using System.Text.Json;
5 | using VueStart.Services;
6 | using System.Text;
7 | using Microsoft.Extensions.Caching.Memory;
8 | using Microsoft.EntityFrameworkCore;
9 | using System.Collections.Generic;
10 | using VueStart.Authorization;
11 |
12 | namespace VueStart.Controllers;
13 |
14 | [Authorize]
15 | [ApiController]
16 | [Route("admin")]
17 | public class AdminController : ControllerBase
18 | {
19 | private readonly ApplicationDbContext dbContext;
20 | private readonly IMemoryCache memoryCache;
21 | private readonly GenerationService generationService;
22 | private readonly GenerationService generateService;
23 | private readonly VisitorStatisticService visitorStatisticService;
24 | private readonly UserService userService;
25 |
26 | public AdminController(ApplicationDbContext dbContext, IMemoryCache memoryCache, GenerationService generationService, GenerationService generateService, VisitorStatisticService visitorStatisticService, UserService userService)
27 | {
28 | this.dbContext = dbContext;
29 | this.memoryCache = memoryCache;
30 | this.generationService = generationService;
31 | this.generateService = generateService;
32 | this.visitorStatisticService = visitorStatisticService;
33 | this.userService = userService;
34 | }
35 |
36 | [AllowAnonymous]
37 | [HttpPost("authenticate")]
38 | public IActionResult Authenticate([FromBody]AuthenticateModel model)
39 | {
40 | var user = userService.Authenticate(model.Username, model.Password);
41 |
42 | if (user == null)
43 | return BadRequest(new { message = "Username or password is incorrect" });
44 |
45 | return Ok(user);
46 | }
47 |
48 | [HttpPost("change-password")]
49 | public IActionResult Authenticate([FromBody]ChangePasswordModel model)
50 | {
51 | var user = userService.Authenticate(model.Username, model.Password);
52 |
53 | if (user == null)
54 | return BadRequest(new { message = "Username or password is incorrect" });
55 |
56 | userService.SetPassword(user, model.NewPassword);
57 |
58 | return Ok(user);
59 | }
60 |
61 |
62 |
63 | [HttpGet]
64 | public IActionResult GetDashboard()
65 | {
66 | return GetFile("index.html");
67 | }
68 |
69 | [HttpGet]
70 | [Route("{fileName}")]
71 | public IActionResult GetFile(string fileName)
72 | {
73 | List visitors = dbContext.Visitors.Include(visitor => visitor.Visits).ToList();
74 | visitors.AddRange(visitorStatisticService.GetCachedVisitors());
75 | if (!visitors.Any())
76 | return NotFound();
77 | JsonDocument doc = JsonDocument.Parse("{\"visitors\":" + JsonSerializer.Serialize(visitors, new JsonSerializerOptions{
78 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
79 | }) + "}");
80 | JsonElement json = doc.RootElement;
81 | string content = "";
82 | string contentType;
83 | var settings = new GenerateSettings {
84 | Frontend = "bootstrap",
85 | IsReadonly = true,
86 | Color = "42b983"
87 | };
88 | var request = new GenerateRequest {
89 | Settings = settings,
90 | Data = json
91 | };
92 | var generator = new VueStartGenerator(request, memoryCache);
93 | string artifactId = generateService.Generate(request, "Dashboard", generator.Id, true, out string appjs, out string indexhtml, true);
94 |
95 | if (string.IsNullOrWhiteSpace(fileName))
96 | fileName = "index.html";
97 |
98 | switch (fileName.Split('.').LastOrDefault())
99 | {
100 | case "js":
101 | contentType = "application/javascript";
102 | content = appjs;
103 | break;
104 | case "html":
105 | contentType = "text/html";
106 | content = indexhtml;
107 | break;
108 | default:
109 | contentType = "text/plain";
110 | break;
111 | }
112 | var cd = new System.Net.Mime.ContentDisposition
113 | {
114 | FileName = fileName,
115 | Inline = true
116 | };
117 | Response.Headers.Add("Content-Disposition", cd.ToString());
118 | Response.Headers.Add("X-Content-Type-Options", "nosniff");
119 | return File(Encoding.UTF8.GetBytes(content), contentType);
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Server/Controllers/ClientErrorsController.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.Extensions.Configuration;
4 | using System.Text.Json;
5 | using System;
6 |
7 | namespace VueStart.Controllers
8 | {
9 | [ApiController]
10 | [Route("api/errors")]
11 | public class ClientErrorsController : ControllerBase
12 | {
13 | private readonly ApplicationDbContext dbContext;
14 |
15 | public ClientErrorsController(ApplicationDbContext dbContext)
16 | {
17 | this.dbContext = dbContext;
18 | }
19 |
20 | [HttpPost]
21 | [Route("")]
22 | public IActionResult LogError([FromBody] JsonElement data) {
23 | dbContext.ClientErrors.Add(new ClientError{
24 | DateTime = DateTime.UtcNow,
25 | UserAgent = Request.Headers["User-Agent"].FirstOrDefault(),
26 | Data = data.ToString()
27 | });
28 | dbContext.SaveChanges();
29 | return Ok();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Server/Controllers/DownloadController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Mvc;
4 | using System.IO;
5 | using System.Text.Json;
6 | using System.IO.Compression;
7 | using VueStart.Services;
8 | using VueStart.Data;
9 | using System.Threading.Channels;
10 |
11 | namespace VueStart.Controllers
12 | {
13 | [ApiController]
14 | [Route("api/download")]
15 | public class DownloadController : ControllerBase
16 | {
17 | private readonly GenerationService generateService;
18 | private readonly Channel eventChannel;
19 |
20 | public DownloadController(GenerationService generateService, Channel eventChannel)
21 | {
22 | this.generateService = generateService;
23 | this.eventChannel = eventChannel;
24 | }
25 |
26 | private static string ToUpperFirst(string type)
27 | {
28 | return Char.ToUpper(type.First()) + type.Substring(1);
29 | }
30 |
31 | [HttpPost]
32 | public IActionResult DownloadEditor([FromBody] GenerateRequest request)
33 | {
34 | var layout = request.Settings.IsReadonly;
35 | var json = request.Data;
36 | try {
37 | var frontend = request.Settings.Frontend.ToFrontendType();
38 | if (frontend == Frontend.None)
39 | return NotFound();
40 | eventChannel.Writer.WriteAsync(new EventData(Request.HttpContext, request, ActionType.Download));
41 | var memoryStream = CreateZipStream(request, "DataTable");
42 | return File(memoryStream, "application/zip", $"{layout}.zip");
43 | } catch (FormatException e) {
44 | return BadRequest(new { error = e.Message });
45 | }
46 | }
47 |
48 | private MemoryStream CreateZipStream(GenerateRequest request, string title)
49 | {
50 | var memoryStream = new MemoryStream();
51 | generateService.Generate(request, title, "", true, out string appjs, out string indexhtml);
52 | using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
53 | {
54 | AddEntry(archive, appjs, "app.js");
55 | AddEntry(archive, indexhtml, "index.html");
56 | }
57 | memoryStream.Position = 0;
58 | return memoryStream;
59 | }
60 |
61 | private static void AddEntry(ZipArchive archive, string content, string fileName)
62 | {
63 | var entry = archive.CreateEntry(fileName);
64 |
65 | using (var entryStream = entry.Open())
66 | using (var streamWriter = new StreamWriter(entryStream))
67 | {
68 | streamWriter.Write(content);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Server/Controllers/FilesController.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.AspNetCore.Mvc;
3 | using BootGen.Core;
4 | using System.IO;
5 | using Microsoft.Extensions.Caching.Memory;
6 | using System.Text;
7 |
8 | namespace VueStart.Controllers
9 | {
10 | [ApiController]
11 | [Route("api/files")]
12 | public class FilesController : ControllerBase
13 | {
14 | private readonly IMemoryCache memoryCache;
15 | public FilesController(IMemoryCache memoryCache)
16 | {
17 | this.memoryCache = memoryCache;
18 | }
19 |
20 | [HttpGet]
21 | [Route("{id}/{filename}")]
22 | public IActionResult ServeFile([FromRoute] string id, [FromRoute] string filename, [FromQuery] bool display) {
23 | if (string.IsNullOrWhiteSpace(filename))
24 | filename = "index.html";
25 | string key = $"{id}/{filename}";
26 | if (display)
27 | key += "_display";
28 | string content;
29 | if (!memoryCache.TryGetValue(key, out content))
30 | return NotFound();
31 |
32 | string contentType;
33 |
34 | switch (filename.Split('.').LastOrDefault())
35 | {
36 | case "js":
37 | contentType = "application/javascript";
38 | break;
39 | case "html":
40 | contentType = "text/html";
41 | break;
42 | default:
43 | contentType = "text/plain";
44 | break;
45 | }
46 | var cd = new System.Net.Mime.ContentDisposition
47 | {
48 | FileName = filename,
49 | Inline = true
50 | };
51 | Response.Headers.Add("Content-Disposition", cd.ToString());
52 | Response.Headers.Add("X-Content-Type-Options", "nosniff");
53 | return File(Encoding.UTF8.GetBytes(content), contentType);
54 | }
55 |
56 | private static VirtualDisk Load(string path)
57 | {
58 | var templates = new VirtualDisk();
59 | foreach (var file in Directory.EnumerateFiles(path))
60 | {
61 | templates.Files.Add(new VirtualFile
62 | {
63 | Name = Path.GetFileName(file),
64 | Path = "",
65 | Content = System.IO.File.ReadAllText(file)
66 | });
67 | }
68 |
69 | return templates;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Server/Controllers/GenerationController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Mvc;
4 | using System.IO;
5 | using System.Text.Json;
6 | using Microsoft.Extensions.Caching.Memory;
7 | using System.IO.Compression;
8 | using VueStart.Services;
9 | using VueStart.Data;
10 | using BootGen.Core;
11 | using System.Threading.Channels;
12 |
13 | namespace VueStart.Controllers
14 | {
15 | [ApiController]
16 | [Route("api/generate")]
17 | public class GenerationController : ControllerBase
18 | {
19 | private readonly GenerationService generationService;
20 | private readonly Channel eventChannel;
21 |
22 | public GenerationController(GenerationService generateService, Channel eventChannel)
23 | {
24 | this.generationService = generateService;
25 | this.eventChannel = eventChannel;
26 | }
27 |
28 | [HttpPost]
29 | public IActionResult Generate([FromBody] GenerateRequest request)
30 | {
31 | var eventData = new EventData(Request.HttpContext, request, ActionType.Generate);
32 | if (request.Data.ValueKind != JsonValueKind.Object) {
33 | eventData.Error = true;
34 | eventChannel.Writer.WriteAsync(eventData);
35 | return BadRequest(new { error = "The root element must be an object!", fixable = true });
36 | }
37 | foreach (var property in request.Data.EnumerateObject()) {
38 | if (property.Value.ValueKind != JsonValueKind.Object && property.Value.ValueKind != JsonValueKind.Array) {
39 | eventData.Error = true;
40 | eventChannel.Writer.WriteAsync(eventData);
41 | return BadRequest(new { error = "Properties of the root element must be an objects or arrays!", fixable = false });
42 | }
43 | }
44 | try {
45 | eventChannel.Writer.WriteAsync(eventData);
46 | var result = generationService.GenerateToCache(request, "DataTable");
47 | return Ok(result);
48 | } catch (FormatException e) {
49 | return BadRequest(new { error = e.Message, fixable = false });
50 | } catch (NamingException e) {
51 | return BadRequest(new { error = e.Message, fixable = true });
52 | }
53 | }
54 |
55 | [HttpPost]
56 | [Route("fix")]
57 | public IActionResult Fix([FromBody] JsonElement json)
58 | {
59 | return Ok(generationService.Fix(json));
60 | }
61 | private static string ToUpperFirst(string type)
62 | {
63 | return Char.ToUpper(type.First()) + type.Substring(1);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Server/Controllers/ShareController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Text.Json;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace VueStart.Controllers;
7 |
8 | [ApiController]
9 | [Route("api/share")]
10 | public class ShareController : ControllerBase
11 | {
12 | private readonly ApplicationDbContext dbContext;
13 |
14 | public ShareController(ApplicationDbContext dbContext)
15 | {
16 | this.dbContext = dbContext;
17 | }
18 | private static int StringHash(string text)
19 | {
20 | unchecked
21 | {
22 | int hash = 23;
23 | foreach (char c in text)
24 | {
25 | hash = hash * 31 + c;
26 | }
27 | return hash;
28 | }
29 | }
30 |
31 | [HttpPost]
32 | public IActionResult Save([FromBody] GenerateRequest request)
33 | {
34 | string requestAsString = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
35 | int hash = StringHash(requestAsString);
36 | var link = dbContext.ShareableLinks.FirstOrDefault(r => r.Hash == hash);
37 | if(link != null) {
38 | return Ok(new { hash = hash });
39 | }
40 | var generateRequest = JsonSerializer.Deserialize(requestAsString);
41 | dbContext.ShareableLinks.Add(new ShareableLink { Hash = hash, GenerateRequest = generateRequest, FirstUse = DateTime.UtcNow, LastUse = DateTime.UtcNow, Count = 0});
42 | dbContext.SaveChanges();
43 | return Ok(new { hash = hash });
44 | }
45 |
46 | [HttpGet]
47 | [Route("{hash}")]
48 | public IActionResult Load([FromRoute] int hash)
49 | {
50 | var link = dbContext.ShareableLinks.FirstOrDefault(r => r.Hash == hash);
51 | if (link == null)
52 | return NotFound();
53 | link.LastUse = DateTime.UtcNow;
54 | link.Count += 1;
55 | dbContext.SaveChanges();
56 | return Ok(link);
57 | }
58 | }
--------------------------------------------------------------------------------
/Server/Data/ActionType.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart.Data
2 | {
3 | public enum ActionType {
4 | Generate,
5 | Download
6 | }
7 | }
--------------------------------------------------------------------------------
/Server/Data/Frontend.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart.Data
2 | {
3 | public enum Frontend {
4 | None,
5 | Bootstrap,
6 | Tailwind,
7 | Vanilla
8 | }
9 |
10 | public static class FrontendTypeExtensions {
11 | public static Frontend ToFrontendType(this string type)
12 | {
13 | switch (type)
14 | {
15 | case "bootstrap":
16 | return Frontend.Bootstrap;
17 | case "tailwind":
18 | return Frontend.Tailwind;
19 | case "vanilla":
20 | return Frontend.Vanilla;
21 | default:
22 | return Frontend.None;
23 | }
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/Server/Entities/ClientError.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace VueStart
4 | {
5 | public class ClientError
6 | {
7 | public int Id { get; set; }
8 | public DateTime DateTime { get; init; }
9 | public string UserAgent { get; set; }
10 | public string Data { get; set; }
11 | }
12 | }
--------------------------------------------------------------------------------
/Server/Entities/InputData.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text.Json;
5 |
6 | namespace VueStart
7 | {
8 |
9 | public class InputData
10 | {
11 | public int Id { get; set; }
12 | public int Hash { get; set; }
13 | public JsonElement Data { get; set; }
14 | public DateTime FirstUse { get; set; }
15 | public DateTime LastUse { get; set; }
16 | public bool Error { get; set; }
17 | public List StatisticRecords { get; set; }
18 | }
19 | }
--------------------------------------------------------------------------------
/Server/Entities/ServerError.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace VueStart
4 | {
5 | public class ServerError
6 | {
7 | public int Id { get; set; }
8 | public DateTime DateTime { get; init; }
9 | public string Message { get; init; }
10 | public string StackTrace { get; init; }
11 | public string File { get; init; }
12 | public int Line { get; init; }
13 | public string Source { get; init; }
14 | public int HResult { get; init; }
15 | public string Data { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/Server/Entities/ShareableLink.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.Json;
3 |
4 | namespace VueStart
5 | {
6 | public class ShareableLink {
7 | public int Id { get; set; }
8 | public int Hash { get; set; }
9 | public JsonElement GenerateRequest { get; set; }
10 | public DateTime FirstUse { get; set; }
11 | public DateTime LastUse { get; set; }
12 | public int Count { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/Server/Entities/StatisticRecord.cs:
--------------------------------------------------------------------------------
1 |
2 | using System;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace VueStart
7 | {
8 |
9 | public class StatisticRecord
10 | {
11 | public int Id { get; set; }
12 | public int InputDataId { get; set; }
13 |
14 | [JsonIgnore]
15 | public InputData InputData { get; set; }
16 | public bool Readonly { get; set; }
17 | public bool Download { get; set; }
18 | public int BootstrapCount { get; set; }
19 | public int TailwindCount { get; set; }
20 | public int VanillaCount { get; set; }
21 |
22 | internal bool IsSameKind(StatisticRecord record)
23 | {
24 | return Readonly == record.Readonly && Download == record.Download;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Server/Entities/User.cs:
--------------------------------------------------------------------------------
1 |
2 | using System.Text.Json.Serialization;
3 |
4 | public class User
5 | {
6 | public int Id { get; set; }
7 | public string Username { get; set; }
8 |
9 | [JsonIgnore]
10 | public string PasswordHash { get; set; }
11 | }
--------------------------------------------------------------------------------
/Server/Entities/Visit.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace VueStart
4 | {
5 | public class Visit {
6 | public int Id { get; set; }
7 | public DateTime Start { get; init; }
8 | public DateTime End { get; set; }
9 | public int Count { get; set; }
10 | }
11 | }
--------------------------------------------------------------------------------
/Server/Entities/Visitor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace VueStart
6 | {
7 | public class Visitor {
8 |
9 | public int Id { get; set; }
10 | public string Token { get; init; }
11 | public string Citation { get; init; }
12 | public DateTime FirstVisit { get; init; }
13 | public string Country { get; set; }
14 | public string Region { get; set; }
15 | public string City { get; set; }
16 | [JsonIgnore]
17 | public string UserAgent { get; set; }
18 | public string OSFamily { get; set; }
19 | public string OSMajor { get; set; }
20 | public string OSMinor { get; set; }
21 | public string DeviceFamily { get; set; }
22 | public string DeviceBrand { get; set; }
23 | public string BrowserFamily { get; set; }
24 | public string BrowserMajor { get; set; }
25 | public string BrowserMinor { get; set; }
26 | public string DeviceModel { get; set; }
27 | public List Visits { get; set; }
28 | }
29 | }
--------------------------------------------------------------------------------
/Server/Migrations/20220527143241_ShareableLinkSettings.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace VueStart.Migrations
6 | {
7 | public partial class ShareableLinkSettings : Migration
8 | {
9 | protected override void Up(MigrationBuilder migrationBuilder)
10 | {
11 | migrationBuilder.DropColumn(
12 | name: "Color",
13 | table: "ShareableLinks");
14 |
15 | migrationBuilder.DropColumn(
16 | name: "Editable",
17 | table: "ShareableLinks");
18 |
19 | migrationBuilder.DropColumn(
20 | name: "FrontendType",
21 | table: "ShareableLinks");
22 |
23 | migrationBuilder.RenameColumn(
24 | name: "Json",
25 | table: "ShareableLinks",
26 | newName: "GenerateRequest");
27 |
28 | migrationBuilder.UpdateData(
29 | table: "Users",
30 | keyColumn: "Id",
31 | keyValue: 1,
32 | column: "PasswordHash",
33 | value: "AQAAAAEAACcQAAAAEIyo9Z9EEDv7Dmyv5tqUNQ6pgWN6HV2kzycBoFanFRBjNxClWbdgQe2BSCq5ldV4Mw==");
34 | }
35 |
36 | protected override void Down(MigrationBuilder migrationBuilder)
37 | {
38 | migrationBuilder.RenameColumn(
39 | name: "GenerateRequest",
40 | table: "ShareableLinks",
41 | newName: "Json");
42 |
43 | migrationBuilder.AddColumn(
44 | name: "Color",
45 | table: "ShareableLinks",
46 | type: "text",
47 | nullable: true);
48 |
49 | migrationBuilder.AddColumn(
50 | name: "Editable",
51 | table: "ShareableLinks",
52 | type: "boolean",
53 | nullable: false,
54 | defaultValue: false);
55 |
56 | migrationBuilder.AddColumn(
57 | name: "FrontendType",
58 | table: "ShareableLinks",
59 | type: "text",
60 | nullable: true);
61 |
62 | migrationBuilder.UpdateData(
63 | table: "Users",
64 | keyColumn: "Id",
65 | keyValue: 1,
66 | column: "PasswordHash",
67 | value: "AQAAAAEAACcQAAAAEP2ySbRDxPLVAzU13JfKBPpxhk2Vjh0LzEe29VP+MuGnKdzeD8BuGR5Dd1oo7gFzzA==");
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Server/Models/AuthenticateModel.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart;
2 |
3 | using System.ComponentModel.DataAnnotations;
4 |
5 | public class AuthenticateModel
6 | {
7 | [Required]
8 | public string Username { get; set; }
9 |
10 | [Required]
11 | public string Password { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/Server/Models/ChangePasswordModel.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart;
2 |
3 | using System.ComponentModel.DataAnnotations;
4 |
5 | public class ChangePasswordModel
6 | {
7 | [Required]
8 | public string Username { get; set; }
9 |
10 | [Required]
11 | public string Password { get; set; }
12 |
13 | [Required]
14 | public string NewPassword { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/Server/Models/EventData.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.AspNetCore.Http;
3 | using VueStart.Data;
4 |
5 | namespace VueStart;
6 |
7 | public class EventData
8 | {
9 | public EventData(HttpContext context, GenerateRequest request, ActionType actionType, bool error = false)
10 | {
11 |
12 | UaString = context.Request.Headers["User-Agent"].FirstOrDefault();
13 | IdToken = context.Request.Headers["idtoken"].FirstOrDefault();
14 | Citation = context.Request.Headers["citation"].FirstOrDefault();
15 | RemoteIpAddress = context.Connection.RemoteIpAddress.ToString();
16 | Request = request;
17 | ActionType = actionType;
18 | Error = error;
19 | }
20 |
21 | public string UaString { get; set; }
22 | public string IdToken { get; set; }
23 | public string Citation { get; set; }
24 | public string RemoteIpAddress { get; set; }
25 | public GenerateRequest Request { get; set; }
26 | public ActionType ActionType { get; set; }
27 | public bool Error { get; set; }
28 | }
29 |
--------------------------------------------------------------------------------
/Server/Models/GenerateRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 |
3 | namespace VueStart;
4 | public class GenerateRequest
5 | {
6 | public GenerateSettings Settings { get; set; }
7 | public JsonElement Data { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/Server/Models/GenerateSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace VueStart;
4 |
5 | public class GenerateSettings
6 | {
7 | public string Frontend { get; set; }
8 | public bool IsReadonly { get; set; }
9 | public string Color { get; set; }
10 | public List ClassSettings { get; set; }
11 | }
--------------------------------------------------------------------------------
/Server/Models/VisitorData.cs:
--------------------------------------------------------------------------------
1 | namespace VueStart;
2 | public struct VisitorData
3 | {
4 | public Visitor Visitor { get; init; }
5 | public string Ip { get; init; }
6 | }
--------------------------------------------------------------------------------
/Server/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Hosting;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace VueStart;
11 |
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/Server/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:63628",
8 | "sslPort": 44356
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "VueStart": {
21 | "commandName": "Project",
22 | "dotnetRunMessages": "true",
23 | "launchBrowser": true,
24 | "launchUrl": "swagger",
25 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Server/Services/ErrorHandlerService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | namespace VueStart
5 | {
6 | class ErrorHandlerService
7 | {
8 | private readonly ApplicationDbContext dbContext;
9 |
10 | public ErrorHandlerService(ApplicationDbContext dbContext)
11 | {
12 | this.dbContext = dbContext;
13 | }
14 |
15 | public void OnException(Exception e, string data)
16 | {
17 | ServerError error = e.InnerException == null ? ExceptionToError(e) : ExceptionToError(e.InnerException);
18 | error.Data = data;
19 | dbContext.ServerErrors.Add(error);
20 | dbContext.SaveChanges();
21 | }
22 |
23 | private static ServerError ExceptionToError(Exception e)
24 | {
25 | var st = new StackTrace(e, true);
26 | var frame = st.GetFrame(0);
27 | return new ServerError {
28 | DateTime = DateTime.UtcNow,
29 | Message = e.Message,
30 | StackTrace = e.StackTrace,
31 | File = frame.GetFileName(),
32 | Line = frame.GetFileLineNumber(),
33 | Source = e.Source,
34 | HResult = e.HResult
35 | };
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/Server/Services/GenerationService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text.Json;
5 | using BootGen.Core;
6 | using Microsoft.Extensions.Caching.Memory;
7 | using Newtonsoft.Json;
8 | using Newtonsoft.Json.Linq;
9 | using System.Drawing;
10 | using System.Linq;
11 |
12 | namespace VueStart.Services;
13 | public class GenerationService
14 | {
15 | private readonly IMemoryCache memoryCache;
16 |
17 | public GenerationService(IMemoryCache memoryCache)
18 | {
19 | this.memoryCache = memoryCache;
20 | }
21 | public string Generate(GenerateRequest request, string title, string generatedId, bool forDownload, out string appjs, out string indexhtml, bool isAdmin = false)
22 | {
23 | var generator = new VueStartGenerator(request, memoryCache);
24 | Generate(request, title, generatedId, forDownload, out appjs, out indexhtml, generator, isAdmin);
25 | return generator.Id;
26 | }
27 |
28 | private static void Generate(GenerateRequest request, string title, string generatedId, bool forDownload, out string appjs, out string indexhtml, VueStartGenerator generator, bool isAdmin = false)
29 | {
30 | string layout = request.Settings.IsReadonly ? "table" : "table-editable";
31 | string templateFileName = $"{request.Settings.Frontend}-{layout}.sbn";
32 | var jsParameters = new Dictionary {
33 | {"classes", generator.DataModel.CommonClasses}
34 | };
35 | if (forDownload)
36 | jsParameters.Add("input", request.Data.ToString());
37 | else
38 | jsParameters.Add("generated_id", $"{generatedId}");
39 |
40 | appjs = generator.Render(templateFileName, jsParameters);
41 | var indexParameters = new Dictionary {
42 | {"title", $"{title}"}
43 | };
44 | if (isAdmin)
45 | indexParameters.Add("base_url", "/admin/");
46 | else if (!forDownload) {
47 | indexParameters.Add("base_url", $"/api/files/{generator.Id}/");
48 | }
49 | indexParameters.Add("color", request.Settings.Color);
50 | if (Brightness(ColorTranslator.FromHtml($"#{request.Settings.Color}")) > 170)
51 | {
52 | indexParameters.Add("text_color", "2c3e50");
53 | }
54 | else
55 | {
56 | indexParameters.Add("text_color", "ffffff");
57 | }
58 | indexParameters.Add("is_readonly", request.Settings.IsReadonly);
59 | indexhtml = generator.Render($"{request.Settings.Frontend}-index.sbn", indexParameters);
60 | }
61 |
62 | private static int Brightness(Color c)
63 | {
64 | return (int)Math.Sqrt(
65 | c.R * c.R * .241 +
66 | c.G * c.G * .691 +
67 | c.B * c.B * .068);
68 | }
69 |
70 | public JsonElement Fix(JsonElement json)
71 | {
72 | if (json.ValueKind == JsonValueKind.Array) {
73 | return JsonDocument.Parse($"{{\"items\": {json} }}").RootElement;
74 | }
75 | var jObject = JsonConvert.DeserializeObject(json.ToString(), new JsonSerializerSettings
76 | {
77 | DateFormatString = "yyyy-MM-ddTHH:mm",
78 | });
79 | try
80 | {
81 | var dataModel = new DataModel
82 | {
83 | TypeToString = TypeScriptGenerator.ToTypeScriptType
84 | };
85 | dataModel.LoadRootObject("App", jObject);
86 | }
87 | catch (NamingException e)
88 | {
89 | string jsonString;
90 | if (e.IsArray)
91 | jsonString = jObject.RenamingArrays(e.ActualName, e.SuggestedName).ToString();
92 | else
93 | jsonString = jObject.RenamingObjects(e.ActualName, e.SuggestedName).ToString();
94 | return JsonDocument.Parse(jsonString).RootElement;
95 | }
96 | return json;
97 | }
98 |
99 | public GenerationResult GenerateToCache(GenerateRequest request, string title)
100 | {
101 | var generator = new VueStartGenerator(request, memoryCache);
102 | Generate(request, title, generator.Id, false, out var appjs, out var indexhtml, generator);
103 | memoryCache.Set($"{generator.Id}/app.js", Minify(appjs), TimeSpan.FromMinutes(30));
104 | memoryCache.Set($"{generator.Id}/index.html", Minify(indexhtml), TimeSpan.FromMinutes(30));
105 | Generate(request, title, generator.Id, true, out var pAppjs, out var pIndexhtml, generator);
106 | memoryCache.Set($"{generator.Id}/app.js_display", pAppjs, TimeSpan.FromMinutes(30));
107 | memoryCache.Set($"{generator.Id}/index.html_display", pIndexhtml, TimeSpan.FromMinutes(30));
108 | var result = new GenerationResult {
109 | Warnings = new List()
110 | };
111 | var warningData = generator.DataModel.Warnings;
112 | foreach (var key in warningData.Keys) {
113 | switch (key) {
114 | case WarningType.EmptyType:
115 | {
116 | HashSet names = warningData[WarningType.EmptyType];
117 | if (names.Count == 1)
118 | result.Warnings.Add($"Empty types are not supported, and are omitted. The type \"{names.First()}\" has no properties.");
119 | else
120 | result.Warnings.Add("Empty types are not supported, and are omitted. The following types have no properties: " + names.Aggregate((a, b) => $"{a}, {b}"));
121 | }
122 | break;
123 | case WarningType.NestedArray:
124 | {
125 | HashSet names = warningData[WarningType.NestedArray];
126 | if (names.Count == 1)
127 | result.Warnings.Add($"Nested arrays are not supported. The property \"{names.First()}\" is omitted.");
128 | else
129 | result.Warnings.Add("Nested arrays are not supported. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}"));
130 | }
131 | break;
132 | case WarningType.PrimitiveArrayElement:
133 | {
134 | HashSet names = warningData[WarningType.PrimitiveArrayElement];
135 | if (names.Count == 1)
136 | result.Warnings.Add($"Arrays with primitive elements are not supported. The property \"{names.First()}\" is omitted.");
137 | else
138 | result.Warnings.Add("Arrays with primitive elements are not supported. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}"));
139 | }
140 | break;
141 | case WarningType.PrimitiveRoot:
142 | {
143 | HashSet names = warningData[WarningType.PrimitiveRoot];
144 | if (names.Count == 1)
145 | result.Warnings.Add($"Root elements must be arrays or objects. The property \"{names.First()}\" is omitted.");
146 | else
147 | result.Warnings.Add("Root elements must be arrays or objects. The following properties are omitted: " + names.Aggregate((a, b) => $"{a}, {b}"));
148 | }
149 | break;
150 | }
151 | }
152 | result.Id = generator.Id;
153 | result.Settings = generator.DataModel.GetSettings().Select(ClassSettings.FromBootGenClassSettings).ToList();
154 | return result;
155 | }
156 |
157 | private string Minify(string value)
158 | {
159 | #if DEBUG
160 | return value;
161 | #else
162 | value = value.Replace("\n", " ");
163 | value = value.Replace("\r", " ");
164 | value = value.Replace("\t", " ");
165 | int length;
166 | do {
167 | length = value.Length;
168 | value = value.Replace(" ", " ");
169 | } while(value.Length != length);
170 |
171 | return value;
172 | #endif
173 | }
174 | }
175 |
176 | public class GenerationResult
177 | {
178 | public string Id { get; set; }
179 | public List Warnings { get; set; }
180 | public List Settings { get; set; }
181 | }
182 |
183 | struct TemplateCacheKey
184 | {
185 | public string Path { get; init; }
186 | }
187 |
188 | class VueStartGenerator
189 | {
190 | public DataModel DataModel { get; }
191 | public string Id { get; }
192 | private readonly TypeScriptGenerator generator;
193 |
194 | private IMemoryCache memoryCache;
195 | public VueStartGenerator(GenerateRequest request, IMemoryCache memoryCache)
196 | {
197 | this.memoryCache = memoryCache;
198 | DataModel = new DataModel
199 | {
200 | TypeToString = TypeScriptGenerator.ToTypeScriptType,
201 | GenerateIds = false
202 | };
203 | var jObject = JsonConvert.DeserializeObject(request.Data.ToString(), new JsonSerializerSettings
204 | {
205 | DateFormatString = "yyyy-MM-ddTHH:mm",
206 | });
207 | DataModel.LoadRootObject("App", jObject, request.Settings.ClassSettings.Select(s => s.ToBootGenClassSettings()).ToList());
208 | Id = Guid.NewGuid().ToString();
209 | generator = new TypeScriptGenerator(null);
210 | generator.Templates = Load("templates");
211 | this.memoryCache = memoryCache;
212 | }
213 |
214 | private VirtualDisk Load(string path)
215 | {
216 | return memoryCache.GetOrCreate(new TemplateCacheKey { Path = path }, entry =>
217 | {
218 | var templates = new VirtualDisk();
219 | foreach (var file in Directory.EnumerateFiles(path))
220 | {
221 | templates.Files.Add(new VirtualFile
222 | {
223 | Name = Path.GetFileName(file),
224 | Path = "",
225 | Content = System.IO.File.ReadAllText(file)
226 | });
227 | }
228 | return templates;
229 | });
230 | }
231 |
232 | internal string Render(string templateFileName, Dictionary parameters)
233 | {
234 | return generator.Render(templateFileName, parameters);
235 | }
236 | }
--------------------------------------------------------------------------------
/Server/Services/GeoLocationService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.Configuration;
8 | using Newtonsoft.Json.Linq;
9 |
10 | namespace VueStart.Services;
11 |
12 | public class GeoLocationService
13 | {
14 | private readonly IConfiguration configuration;
15 |
16 | public GeoLocationService(IConfiguration configuration)
17 | {
18 | this.configuration = configuration;
19 | }
20 |
21 | public async Task SetGeoLocation(List data)
22 | {
23 | var ipInfotoken = configuration.GetValue("IpInfoToken");
24 | if (string.IsNullOrWhiteSpace(ipInfotoken))
25 | return;
26 |
27 | using var client = new HttpClient();
28 | string ipListString = data.Select(d => $"\"{d.Ip}\"").Aggregate((a, b) => $"{a}, {b}");
29 | string stringData = $"[{ipListString}]";
30 | var content = new StringContent(stringData, Encoding.UTF8, "application/json");
31 |
32 | using var response = await client.PostAsync($"https://ipinfo.io/batch?token={ipInfotoken}", content);
33 | using var reader = new StreamReader(response.Content.ReadAsStream());
34 |
35 | var jsonString = reader.ReadToEnd();
36 | var jObject = JObject.Parse(jsonString);
37 | foreach (var item in data) {
38 | var obj = jObject.GetValue(item.Ip) as JObject;
39 | if (obj != null) {
40 | SetLocation(item.Visitor, obj);
41 | }
42 | }
43 | }
44 |
45 | private static void SetLocation(Visitor visitor, JObject jObject)
46 | {
47 | visitor.Country = jObject.GetValue("country")?.ToString();
48 | visitor.Region = jObject.GetValue("region")?.ToString();
49 | visitor.City = jObject.GetValue("city")?.ToString();
50 | }
51 | }
--------------------------------------------------------------------------------
/Server/Services/InputStatisticService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Microsoft.Extensions.Logging;
5 | using VueStart.Data;
6 |
7 | namespace VueStart.Services;
8 |
9 | public class InputStatisticService
10 | {
11 | private readonly ILogger logger;
12 |
13 | private List InputData { get; } = new List();
14 |
15 | public InputStatisticService(ILogger logger)
16 | {
17 | this.logger = logger;
18 | }
19 |
20 | private static int StringHash(string text)
21 | {
22 | unchecked
23 | {
24 | int hash = 23;
25 | foreach (char c in text)
26 | {
27 | hash = hash * 31 + c;
28 | }
29 | return hash;
30 | }
31 | }
32 |
33 |
34 | public void StoreStatisticRecord(GenerateRequest request, ActionType actionType, bool error)
35 | {
36 | int hash = StringHash(request.Data.ToString());
37 | var inputData = InputData.FirstOrDefault(r => r.Hash == hash);
38 | var record = new StatisticRecord {
39 | Download = actionType == ActionType.Download,
40 | Readonly = request.Settings.IsReadonly
41 | };
42 | if (inputData == null)
43 | {
44 | inputData = new InputData {
45 | Hash = hash,
46 | Data = request.Data,
47 | FirstUse = DateTime.UtcNow,
48 | LastUse = DateTime.UtcNow,
49 | Error = error,
50 | StatisticRecords = new List() { record }
51 | };
52 | InputData.Add(inputData);
53 | } else {
54 | var existingRecord = inputData.StatisticRecords.FirstOrDefault(r => r.IsSameKind(record));
55 | if (existingRecord != null)
56 | {
57 | record = existingRecord;
58 | } else {
59 | inputData.StatisticRecords.Add(record);
60 | }
61 | }
62 | UpdateRecord(record, request.Settings.Frontend.ToFrontendType());
63 | }
64 |
65 | private void UpdateRecord(StatisticRecord record, Frontend cssType)
66 | {
67 | switch (cssType)
68 | {
69 | case Frontend.Bootstrap:
70 | record.BootstrapCount += 1;
71 | break;
72 | case Frontend.Tailwind:
73 | record.TailwindCount += 1;
74 | break;
75 | case Frontend.Vanilla:
76 | record.VanillaCount += 1;
77 | break;
78 | }
79 | }
80 |
81 | public void SaveRecords(ApplicationDbContext dbContext)
82 | {
83 | logger.Log(LogLevel.Information, $"Saving {InputData.Count} input records.");
84 | foreach (var inputData in InputData)
85 | {
86 | var existingInputData = dbContext.InputData.FirstOrDefault(r => r.Hash == inputData.Hash);
87 | if (existingInputData != null)
88 | {
89 | dbContext.Entry(existingInputData).Collection(i => i.StatisticRecords).Load();
90 | existingInputData.LastUse = DateTime.UtcNow;
91 | foreach(var record in inputData.StatisticRecords) {
92 | var existingRecord = existingInputData.StatisticRecords.FirstOrDefault(r => r.IsSameKind(record));
93 | if (existingRecord != null) {
94 | existingRecord.BootstrapCount += record.BootstrapCount;
95 | existingRecord.TailwindCount += record.TailwindCount;
96 | existingRecord.VanillaCount += record.VanillaCount;
97 | } else {
98 | existingInputData.StatisticRecords.Add(record);
99 | }
100 | }
101 | }
102 | else
103 | {
104 | dbContext.InputData.Add(inputData);
105 | }
106 | }
107 | InputData.Clear();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Server/Services/StatisticsService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Channels;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 | using Microsoft.Extensions.Logging;
8 |
9 | namespace VueStart.Services;
10 |
11 | public class StatisticsService : BackgroundService
12 | {
13 | private readonly VisitorStatisticService visitorStatisticService;
14 | private readonly InputStatisticService inputStatisticService;
15 | private readonly IServiceProvider serviceProvider;
16 | private readonly Channel eventChannel;
17 | private readonly ILogger logger;
18 | private int currentDay = -1;
19 | private int currentPeriod = -1;
20 | public StatisticsService(VisitorStatisticService visitorStatisticService, InputStatisticService inputStatisticService, IServiceProvider serviceProvider, Channel eventChannel, ILogger logger, IHostApplicationLifetime applicationLifetime)
21 | {
22 | this.visitorStatisticService = visitorStatisticService;
23 | this.inputStatisticService = inputStatisticService;
24 | this.serviceProvider = serviceProvider;
25 | this.eventChannel = eventChannel;
26 | this.logger = logger;
27 | applicationLifetime.ApplicationStopping.Register(() => {
28 | logger.Log(LogLevel.Information, "Application stopping detected.");
29 | SaveData().Wait();
30 | });
31 | }
32 |
33 | private async Task OnEvent(EventData eventData)
34 | {
35 | try {
36 | var now = DateTime.UtcNow;
37 | #if DEBUG
38 | var periodLengthInMinutes = 1;
39 | #else
40 | var periodLengthInMinutes = 15;
41 | #endif
42 | int day = (now - new DateTime(2021, 1, 1)).Days;
43 | int period = (int)now.TimeOfDay.TotalMinutes / periodLengthInMinutes;
44 | if (currentDay != -1 && (day != currentDay || period != currentPeriod))
45 | {
46 | await SaveData();
47 | }
48 | currentDay = day;
49 | currentPeriod = period;
50 | visitorStatisticService.StoreVisit(eventData);
51 | inputStatisticService.StoreStatisticRecord(eventData.Request, eventData.ActionType, eventData.Error);
52 | } catch (Exception e) {
53 | using IServiceScope scope = serviceProvider.CreateScope();
54 | var errorHandlingService = scope.ServiceProvider.GetRequiredService();
55 | errorHandlingService.OnException(e, null);
56 | logger.Log(LogLevel.Error, e, e.Message);
57 | }
58 | }
59 |
60 | private async Task SaveData()
61 | {
62 | using IServiceScope scope = serviceProvider.CreateScope();
63 |
64 | var dbContext = scope.ServiceProvider.GetRequiredService();
65 | try {
66 | await visitorStatisticService.SaveVisitors(dbContext);
67 | inputStatisticService.SaveRecords(dbContext);
68 | dbContext.SaveChanges();
69 | } catch (Exception e) {
70 | var errorHandlingService = scope.ServiceProvider.GetRequiredService();
71 | errorHandlingService.OnException(e, null);
72 | logger.Log(LogLevel.Error, e, e.Message);
73 | }
74 | }
75 |
76 |
77 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
78 | {
79 | await foreach(var e in eventChannel.Reader.ReadAllAsync(stoppingToken)) {
80 | logger.Log(LogLevel.Information, "Event data read.");
81 | await OnEvent(e);
82 | }
83 | logger.Log(LogLevel.Information, "Event log finished.");
84 | await SaveData();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Server/Services/UserService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Identity;
6 |
7 | namespace VueStart.Services;
8 |
9 | public class UserService
10 | {
11 | private readonly ApplicationDbContext dbContext;
12 |
13 | public UserService(ApplicationDbContext dbContext){
14 | this.dbContext = dbContext;
15 | }
16 |
17 | public User Authenticate(string username, string password)
18 | {
19 | var user = dbContext.Users.FirstOrDefault(u => u.Username == username);
20 | if (user == null)
21 | return null;
22 | var passwordHasher = new PasswordHasher();
23 | var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
24 |
25 | switch (result)
26 | {
27 | case PasswordVerificationResult.Failed:
28 | return null;
29 | case PasswordVerificationResult.SuccessRehashNeeded:
30 | user.PasswordHash = passwordHasher.HashPassword(user, password);
31 | dbContext.SaveChanges();
32 | break;
33 | }
34 |
35 | return user;
36 | }
37 |
38 | internal void SetPassword(User user, string newPassword)
39 | {
40 | var passwordHasher = new PasswordHasher();
41 | user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
42 | dbContext.SaveChanges();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Server/Services/VisitorStatisticService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.Logging;
7 | using UAParser;
8 |
9 | namespace VueStart.Services;
10 |
11 | public class VisitorStatisticService
12 | {
13 | private Dictionary Visitors { get; } = new Dictionary();
14 | private readonly GeoLocationService geoLocationService;
15 | private readonly ILogger logger;
16 |
17 | public VisitorStatisticService(GeoLocationService geoLocationService, ILogger logger)
18 | {
19 | this.geoLocationService = geoLocationService;
20 | this.logger = logger;
21 | }
22 |
23 | public void StoreVisit(EventData eventData)
24 | {
25 | if (Visitors.TryGetValue(eventData.IdToken, out var data))
26 | {
27 | var visit = data.Visitor.Visits.First();
28 | visit.Count += 1;
29 | visit.End = DateTime.UtcNow;
30 | }
31 | else
32 | {
33 | var visitor = CreateVisitor(eventData.UaString, eventData.IdToken, eventData.Citation);
34 | Visitors.Add(eventData.IdToken, new VisitorData {
35 | Visitor = visitor,
36 | Ip = eventData.RemoteIpAddress
37 | });
38 |
39 | visitor.Visits = new List {
40 | new Visit {
41 | Start = DateTime.UtcNow,
42 | End = DateTime.UtcNow,
43 | Count = 1
44 | }
45 | };
46 | }
47 | }
48 |
49 | public async Task SaveVisitors(ApplicationDbContext dbContext)
50 | {
51 | var toLocate = new List();
52 | logger.Log(LogLevel.Information, $"Saving {Visitors.Count} visitors.");
53 | foreach (var item in Visitors)
54 | {
55 | var visitor = item.Value.Visitor;
56 | var earlierVisitor = dbContext.Visitors.FirstOrDefault(v => v.Token == visitor.Token);
57 | if (earlierVisitor != null)
58 | {
59 | dbContext.Entry(earlierVisitor).Collection(v => v.Visits).Load();
60 | earlierVisitor.Visits.AddRange(visitor.Visits);
61 | }
62 | else
63 | {
64 | toLocate.Add(item.Value);
65 | }
66 | }
67 | if (toLocate.Any()) {
68 | await geoLocationService.SetGeoLocation(toLocate);
69 | foreach (var item in toLocate) {
70 | dbContext.Visitors.Add(item.Visitor);
71 | }
72 | }
73 | Visitors.Clear();
74 | }
75 |
76 | internal IEnumerable GetCachedVisitors()
77 | {
78 | return Visitors.Values.Select(d => d.Visitor);
79 | }
80 |
81 | private Visitor CreateVisitor(string uaString, string token, string citation)
82 | {
83 | var uaParser = Parser.GetDefault();
84 | ClientInfo c = uaParser.Parse(uaString);
85 | var visitor = new Visitor
86 | {
87 | Token = token,
88 | Citation = citation,
89 | FirstVisit = DateTime.UtcNow,
90 | UserAgent = uaString,
91 | OSFamily = c.OS.Family,
92 | OSMajor = c.OS.Major,
93 | OSMinor = c.OS.Minor,
94 | DeviceBrand = c.Device.Brand,
95 | DeviceFamily = c.Device.Family,
96 | DeviceModel = c.Device.Model,
97 | BrowserFamily = c.UA.Family,
98 | BrowserMajor = c.UA.Major,
99 | BrowserMinor = c.UA.Minor
100 | };
101 | logger.Log(LogLevel.Information, "New visitor");
102 | return visitor;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Server/Settings/ClassSettings.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace VueStart;
5 | public class ClassSettings
6 | {
7 | public string Name { get; set; }
8 | public List PropertySettings { get; set; }
9 |
10 | public static ClassSettings FromBootGenClassSettings(BootGen.Core.ClassSettings classSettings)
11 | {
12 | return new ClassSettings
13 | {
14 | Name = classSettings.Name,
15 | PropertySettings = classSettings.PropertySettings.Select(VueStart.PropertySettings.FromBootGenPropertySettings).ToList()
16 | };
17 | }
18 |
19 | public BootGen.Core.ClassSettings ToBootGenClassSettings()
20 | {
21 | return new BootGen.Core.ClassSettings
22 | {
23 | Name = Name,
24 | PropertySettings = PropertySettings.Select(s => s.ToBootGenPropertySettings()).ToList()
25 | };
26 | }
27 | }
--------------------------------------------------------------------------------
/Server/Settings/PropertySettings.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace VueStart;
3 |
4 | public struct PropertySettings
5 | {
6 | public string Name { get; set; }
7 | public string VisibleName { get; set; }
8 | public bool? IsReadOnly { get; set; }
9 | public bool IsHidden { get; set; }
10 | public bool? ShowAsImage { get; set; }
11 |
12 | public static PropertySettings FromBootGenPropertySettings(BootGen.Core.PropertySettings propertySettings)
13 | {
14 | return new PropertySettings
15 | {
16 | Name = propertySettings.Name,
17 | VisibleName = propertySettings.VisibleName,
18 | IsReadOnly = propertySettings.IsReadOnly,
19 | IsHidden = propertySettings.IsHidden,
20 | ShowAsImage = propertySettings.ShowAsImage
21 | };
22 | }
23 |
24 | public BootGen.Core.PropertySettings ToBootGenPropertySettings()
25 | {
26 | return new BootGen.Core.PropertySettings
27 | {
28 | Name = Name,
29 | VisibleName = VisibleName,
30 | IsReadOnly = IsReadOnly,
31 | IsHidden = IsHidden,
32 | ShowAsImage = ShowAsImage
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Server/Startup.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Diagnostics;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.AspNetCore.HttpOverrides;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using VueStart.Services;
10 | using Microsoft.AspNetCore.Http;
11 | using VueStart.Authorization;
12 | using System.Threading.Channels;
13 | using Microsoft.EntityFrameworkCore;
14 |
15 | namespace VueStart
16 | {
17 | public class Startup
18 | {
19 | public Startup(IConfiguration configuration)
20 | {
21 | Configuration = configuration;
22 | }
23 |
24 | public IConfiguration Configuration { get; }
25 |
26 | // This method gets called by the runtime. Use this method to add services to the container.
27 | public void ConfigureServices(IServiceCollection services)
28 | {
29 | services.AddControllers();
30 | services.AddMemoryCache();
31 | services.AddHostedService();
32 | services.AddSingleton(Channel.CreateUnbounded());
33 | services.AddSingleton();
34 | services.AddSingleton();
35 | services.AddSingleton();
36 | services.AddScoped();
37 | services.AddScoped();
38 | services.AddScoped();
39 | services.AddDbContext(options => options.UseNpgsql(Configuration.GetConnectionString("PostgreSQL")));
40 | }
41 |
42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
44 | {
45 | if (env.IsDevelopment())
46 | {
47 | app.UseDeveloperExceptionPage();
48 | }
49 |
50 | app.UseForwardedHeaders(new ForwardedHeadersOptions
51 | {
52 | ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
53 | });
54 | app.Use((context, next) =>
55 | {
56 | context.Request.EnableBuffering();
57 | return next();
58 | });
59 | app.UseExceptionHandler(builder => {
60 | builder.Run(async context => {
61 | using var scope = app.ApplicationServices.CreateScope();
62 | var service = scope.ServiceProvider.GetRequiredService();
63 | var handler = context.Features.Get();
64 | var exception = handler?.Error;
65 | context.Request.Body.Seek(0, SeekOrigin.Begin);
66 | if (exception != null) {
67 | service.OnException(exception, await new StreamReader(context.Request.Body).ReadToEndAsync());
68 | }
69 | });
70 | });
71 |
72 | //app.UseHttpsRedirection();
73 |
74 | app.UseRouting();
75 |
76 | app.UseMiddleware();
77 | app.UseCors();
78 |
79 | app.UseEndpoints(endpoints =>
80 | {
81 | endpoints.MapControllers();
82 | });
83 |
84 |
85 | if (env.IsDevelopment())
86 | {
87 | app.UseSpa(spa =>
88 | {
89 | spa.UseProxyToSpaDevelopmentServer($"http://localhost:8080");
90 | });
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Server/VueStart.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 | all
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | PreserveNewest
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Server/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "PostgreSQL": "User ID =postgres;Password=secret;Server=localhost;Port=5432;Database=vuestart;Integrated Security=true;Pooling=true;"
11 | },
12 | "Urls": "http://localhost:5000"
13 | }
14 |
--------------------------------------------------------------------------------
/Server/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/Server/templates/bootstrap-index.sbn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ title }}
8 | {{~ if base_url ~}}
9 |
10 | {{~ end ~}}
11 |
12 |
13 |
78 |
79 |
80 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Server/templates/tailwind-index.sbn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
22 | {{ title }}
23 | {{~ if base_url ~}}
24 |
25 | {{~ end ~}}
26 |
41 |
42 |
43 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Server/templates/vanilla-index.sbn:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ title }}
9 | {{~ if base_url ~}}
10 |
11 | {{~ end ~}}
12 |
228 |
229 |
230 |
233 |
234 |
235 |
236 |
237 |
238 |
--------------------------------------------------------------------------------
/vs_demo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BootGen/VueStart/93e29fd47e1d20ebf104989f57bc826ff4f7f0df/vs_demo.webp
--------------------------------------------------------------------------------