├── .browserslistrc ├── .env.development ├── .env.production ├── .eslintrc.js ├── .github └── workflows │ ├── azure-static-web-apps-mango-beach-029eda510.yml │ └── azure-static-web-apps-white-flower-0d5d70100.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── README.md ├── babel.config.js ├── docs └── localhost_serve.png ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── images │ ├── login.png │ ├── logo.png │ └── no-image.png └── index.html ├── src ├── App.vue ├── assets │ ├── banner-logo-large.png │ ├── banner-logo-small.png │ ├── banner.png │ ├── bg.png │ ├── bg@2x.png │ ├── bg@3x.png │ ├── burger.svg │ ├── close.png │ ├── close.svg │ ├── close@2x.png │ ├── close@3x.png │ ├── drag-handle.png │ ├── logo-beta.svg │ └── logo.svg ├── components │ ├── LinkList.vue │ ├── LinkListItem.vue │ ├── ListDetails.vue │ ├── ListForm.vue │ ├── Modal.vue │ ├── ModalDelete.vue │ ├── ModalLogin.vue │ ├── NavBar.vue │ ├── NewLink.vue │ ├── NotFound.vue │ ├── Notification.vue │ ├── ProgressBar.vue │ ├── QrCode.vue │ └── UserMenu.vue ├── config.ts ├── directives │ └── blurOnEnterKey.ts ├── main.ts ├── models │ ├── IAuthResponse.ts │ ├── ILink.ts │ ├── IListResponse.ts │ ├── IOGData.ts │ ├── IUserList.ts │ ├── Link.ts │ ├── List.ts │ └── User.ts ├── references.d.ts ├── router.ts ├── services │ ├── api.service.ts │ ├── list.service.ts │ └── user.service.ts ├── shared │ └── Array.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ ├── AppModule.ts │ ├── ListModule.ts │ ├── UserModule.ts │ └── store.ts ├── styles │ ├── bulma-custom.scss │ ├── site.scss │ └── variables.scss └── views │ ├── Edit.vue │ ├── Home.vue │ ├── List.vue │ └── User.vue ├── tests └── load-test.yml ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_FRONTEND=http://127.0.0.1:8080 2 | VUE_APP_FUNCTION_KEY=SteUyT0o2YSt4JW8k2qZ5ShTBcQYQj1pxE8cO7oTrYI4ps92/53o9w== 3 | VUE_APP_BACKEND=https://www.theurlist.com 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_FRONTEND=https://www.theurlist.com 2 | VUE_APP_FUNCTION_KEY=SteUyT0o2YSt4JW8k2qZ5ShTBcQYQj1pxE8cO7oTrYI4ps92/53o9w== 3 | VUE_APP_BACKEND=https://www.theurlist.com 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "@typescript-eslint/parser" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-mango-beach-029eda510.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | lfs: false 22 | - name: Build And Deploy 23 | id: builddeploy 24 | uses: Azure/static-web-apps-deploy@v1 25 | with: 26 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_BEACH_029EDA510 }} 27 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 28 | action: "upload" 29 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 30 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 31 | app_location: "/" # App source code path 32 | api_location: "" # Api source code path - optional 33 | output_location: "dist" # Built app content directory - optional 34 | ###### End of Repository/Build Configurations ###### 35 | 36 | close_pull_request_job: 37 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 38 | runs-on: ubuntu-latest 39 | name: Close Pull Request Job 40 | steps: 41 | - name: Close Pull Request 42 | id: closepullrequest 43 | uses: Azure/static-web-apps-deploy@v1 44 | with: 45 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_BEACH_029EDA510 }} 46 | action: "close" 47 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-white-flower-0d5d70100.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build And Deploy 22 | id: builddeploy 23 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 24 | with: 25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_FLOWER_0D5D70100 }} 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 27 | action: "upload" 28 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 30 | app_location: "/" # App source code path 31 | api_location: "api" # Api source code path - optional 32 | output_location: "" # Built app content directory - optional 33 | ###### End of Repository/Build Configurations ###### 34 | 35 | close_pull_request_job: 36 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 37 | runs-on: ubuntu-latest 38 | name: Close Pull Request Job 39 | steps: 40 | - name: Close Pull Request 41 | id: closepullrequest 42 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 43 | with: 44 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_FLOWER_0D5D70100 }} 45 | action: "close" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | 5 | # local env files 6 | **/.env.local 7 | **/.env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | -------------------------------------------------------------------------------- /.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 | "type": "chrome", 9 | "request": "launch", 10 | "name": "vuejs: chrome", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}/src", 13 | "breakOnLoad": true, 14 | "sourceMapPathOverrides": { 15 | "webpack:///./src/*": "${webRoot}/*" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "vetur.format.defaultFormatter.html": "prettier", 4 | "vetur.format.defaultFormatter.css": "prettier", 5 | "vetur.format.defaultFormatter.scss": "prettier", 6 | "vetur.format.defaultFormatter.js": "prettier", 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "serve", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | WORKDIR /app 4 | 5 | # Copy in the static build assets 6 | COPY dist/ /app/ 7 | 8 | # Copy in the nginx config file 9 | COPY nginx.conf /etc/nginx/nginx.conf 10 | 11 | # All files are in, start the web server 12 | CMD ["nginx"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Urlist - Frontend 2 | [![Build status](https://burkeknowswords.visualstudio.com/The%20Urlist/_apis/build/status/Frontend%20Build)](https://burkeknowswords.visualstudio.com/The%20Urlist/_build/latest?definitionId=7) 3 | 4 | The frontend for this project was build with the following libraries and frameworks: 5 | * [TypeScript](https://www.typescriptlang.org/) 6 | * [Vue.js](https://github.com/vuejs/vue) / [Vue CLI](https://github.com/vuejs/vue-cli) 7 | * [Vuelidate](https://github.com/vuelidate/vuelidate) 8 | * [Axios](https://github.com/axios/axios) 9 | 10 | Other useful tools 11 | * [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=theurlist-github-buhollan) 12 | * [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur&WT.mc_id=theurlist-github-buhollan) 13 | * [VS Code Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome&WT.mc_id=theurlist-github-buhollan) 14 | * [Vue VS Code Extension Pack](https://marketplace.visualstudio.com/items?itemName=sdras.vue-vscode-extensionpack&WT.mc_id=theurlist-github-buhollan) 15 | * [Vue browser devtools](https://github.com/vuejs/vue-devtools) 16 | 17 | 18 | ## Build and run the frontend locally 19 | 20 | ### Install Vue CLI globally 21 | ```bash 22 | npm install -g @vue/cli 23 | ``` 24 | 25 | ### Install npm packages for frontend project 26 | ```bash 27 | npm install 28 | ``` 29 | 30 | ### Serve development build 31 | 32 | ```bash 33 | npm run serve 34 | ``` 35 | ![localhost serve](/docs/localhost_serve.png) 36 | 37 | ### Create production build 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | *This creates a dist folder under frontend* 43 | 44 | ### Lints and fixes files 45 | 46 | ```bash 47 | npm run lint 48 | ``` 49 | ### Running locally vs running on Azure 50 | The code is optimised to be run in a local environment. If either the frontend or backend are run on Azure, there is one line of code that needs to be changed: 51 | In \frontend\src\services\api.service.ts change line 19 from 52 | 53 | ```bash 54 | axios.defaults.withCredentials = false; 55 | ``` 56 | to 57 | 58 | ```bash 59 | axios.defaults.withCredentials = true; 60 | ``` 61 | This should keep you out of CORS troubles 62 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /docs/localhost_serve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/docs/localhost_serve.png -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | 3 | # Set number of worker processes automatically based on number of CPU cores. 4 | worker_processes auto; 5 | 6 | # Enables the use of JIT for regular expressions to speed-up their processing. 7 | pcre_jit on; 8 | 9 | # Specify where the PID of nginx will be written 10 | pid /nginx.pid; 11 | 12 | # Run in foreground 13 | daemon off; 14 | 15 | # Configures default error logger. 16 | error_log /var/log/nginx/error.log warn; 17 | 18 | events { 19 | # The maximum number of simultaneous connections that can be opened by 20 | # a worker process. 21 | worker_connections 1024; 22 | } 23 | 24 | http { 25 | # Includes mapping of file name extensions to MIME types of responses 26 | # and defines the default type. 27 | include /etc/nginx/mime.types; 28 | default_type application/octet-stream; 29 | 30 | # Name servers used to resolve names of upstream servers into addresses. 31 | # It's also needed when using tcpsocket and udpsocket in Lua modules. 32 | #resolver 208.67.222.222 208.67.220.220; 33 | 34 | # Don't tell nginx version to clients. 35 | server_tokens off; 36 | 37 | # Specifies the maximum accepted body size of a client request, as 38 | # indicated by the request header Content-Length. If the stated content 39 | # length is greater than this size, then the client receives the HTTP 40 | # error code 413. Set to 0 to disable. 41 | client_max_body_size 1m; 42 | 43 | # Timeout for keep-alive connections. Server will close connections after 44 | # this time. 45 | keepalive_timeout 65; 46 | 47 | # Sendfile copies data between one FD and other from within the kernel, 48 | # which is more efficient than read() + write(). 49 | sendfile on; 50 | 51 | # Don't buffer data-sends (disable Nagle algorithm). 52 | # Good for sending frequent small bursts of data in real time. 53 | tcp_nodelay on; 54 | 55 | # Causes nginx to attempt to send its HTTP response head in one packet, 56 | # instead of using partial frames. 57 | #tcp_nopush on; 58 | 59 | # Enable gzipping of responses. 60 | gzip on; 61 | 62 | # Set the Vary HTTP header as defined in the RFC 2616. 63 | gzip_vary on; 64 | 65 | # Enable checking the existence of precompressed files. 66 | #gzip_static on; 67 | 68 | # Specifies the main log format. 69 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 70 | '$status $body_bytes_sent "$http_referer" ' 71 | '"$http_user_agent" "$http_x_forwarded_for"'; 72 | 73 | # Sets the path, format, and configuration for a buffered log write. 74 | access_log /var/log/nginx/access.log main; 75 | 76 | server { 77 | location / { 78 | root /app; 79 | try_files $uri $uri/ /index.html; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the-urlist", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --host 127.0.0.1", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.1", 12 | "cuid": "^2.1.6", 13 | "qrcode.vue": "^1.7.0", 14 | "typescript-debounce-decorator": "0.0.17", 15 | "v-tooltip": "^2.0.2", 16 | "vue": "^2.6.10", 17 | "vue-class-component": "^7.1.0", 18 | "vue-property-decorator": "^8.1.1", 19 | "vue-router": "^3.0.6", 20 | "vue-slicksort": "^1.1.3", 21 | "vuelidate": "^0.7.4", 22 | "vuex": "^3.1.1" 23 | }, 24 | "devDependencies": { 25 | "@fortawesome/fontawesome-free": "^5.9.0", 26 | "@types/vuelidate": "^0.7.5", 27 | "@vue/cli-plugin-babel": "^3.8.0", 28 | "@vue/cli-plugin-eslint": "^3.8.0", 29 | "@vue/cli-plugin-typescript": "^3.8.1", 30 | "@vue/cli-service": "^3.8.0", 31 | "@vue/eslint-config-prettier": "^4.0.1", 32 | "@vue/eslint-config-typescript": "^4.0.0", 33 | "babel-eslint": "^10.0.1", 34 | "bulma": "^0.7.5", 35 | "eslint": "^5.16.0", 36 | "eslint-plugin-vue": "^5.2.2", 37 | "node-sass": "^4.14.0", 38 | "sass-loader": "^7.1.0", 39 | "typescript": "~3.4.4", 40 | "typescript-eslint-parser": "^22.0.0", 41 | "vue-template-compiler": "^2.6.10", 42 | "vuex-module-decorators": "^0.9.9" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/public/favicon.ico -------------------------------------------------------------------------------- /public/images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/public/images/login.png -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/public/images/logo.png -------------------------------------------------------------------------------- /public/images/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/public/images/no-image.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 17 | 21 | 22 | The Urlist 23 | 88 | 89 | 90 | 96 |
97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /src/assets/banner-logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/banner-logo-large.png -------------------------------------------------------------------------------- /src/assets/banner-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/banner-logo-small.png -------------------------------------------------------------------------------- /src/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/banner.png -------------------------------------------------------------------------------- /src/assets/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/bg.png -------------------------------------------------------------------------------- /src/assets/bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/bg@2x.png -------------------------------------------------------------------------------- /src/assets/bg@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/bg@3x.png -------------------------------------------------------------------------------- /src/assets/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Medium Pub Banner 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/close.png -------------------------------------------------------------------------------- /src/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/close@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/close@2x.png -------------------------------------------------------------------------------- /src/assets/close@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/close@3x.png -------------------------------------------------------------------------------- /src/assets/drag-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/drag-handle.png -------------------------------------------------------------------------------- /src/assets/logo-beta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled 7 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 | 27 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/components/LinkList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 52 | -------------------------------------------------------------------------------- /src/components/LinkListItem.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 115 | 116 | 117 | 209 | -------------------------------------------------------------------------------- /src/components/ListDetails.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 155 | 156 | 157 | 187 | -------------------------------------------------------------------------------- /src/components/ListForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 86 | 87 | 88 | 97 | -------------------------------------------------------------------------------- /src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | 34 | 35 | 56 | -------------------------------------------------------------------------------- /src/components/ModalDelete.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 53 | 54 | 55 | 60 | -------------------------------------------------------------------------------- /src/components/ModalLogin.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 42 | 43 | 44 | 53 | -------------------------------------------------------------------------------- /src/components/NavBar.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 146 | 147 | 148 | 189 | -------------------------------------------------------------------------------- /src/components/NewLink.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 58 | -------------------------------------------------------------------------------- /src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | 45 | 78 | -------------------------------------------------------------------------------- /src/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | 31 | 32 | 54 | -------------------------------------------------------------------------------- /src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | 86 | -------------------------------------------------------------------------------- /src/components/QrCode.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/UserMenu.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 26 | 77 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | FRONTEND: process.env.VUE_APP_FRONTEND, 3 | BACKEND: process.env.VUE_APP_BACKEND, 4 | AUTH_URL: function(provider: string) { 5 | return `${ 6 | process.env.VUE_APP_BACKEND 7 | }/.auth/login/${provider}?post_login_redirect_url=${ 8 | process.env.VUE_APP_FRONTEND 9 | }`; 10 | }, 11 | LOGOUT_URL: `${ 12 | process.env.VUE_APP_BACKEND 13 | }/.auth/logout?post_logout_redirect_uri=${process.env.VUE_APP_FRONTEND}`, 14 | FUNCTION_KEY: process.env.VUE_APP_FUNCTION_KEY 15 | }; 16 | -------------------------------------------------------------------------------- /src/directives/blurOnEnterKey.ts: -------------------------------------------------------------------------------- 1 | const blurOnEnterKey = { 2 | bind(el: HTMLElement) { 3 | // el might not be present for server-side rendering. 4 | if (el) { 5 | // just remove the focus from the element 6 | el.onkeypress = (ev: KeyboardEvent) => { 7 | if (ev.which === 13) { 8 | el.blur(); 9 | } 10 | }; 11 | } 12 | } 13 | }; 14 | 15 | export default blurOnEnterKey; 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store/store"; 5 | 6 | // import styles 7 | import "@/styles/site.scss"; 8 | 9 | // directives 10 | import blurOnEnterKey from "@/directives/blurOnEnterKey"; 11 | Vue.directive("blur-on-enter-key", blurOnEnterKey); 12 | 13 | // Initialize API which has some global settings which will be kept in memory 14 | import ApiService from "@/services/api.service"; 15 | ApiService.init(); 16 | 17 | // The base JavaScript array type is overriden to provide for easier retrieval of 18 | // members by their id. 19 | import "@/shared/Array"; 20 | 21 | // No idea what this does 22 | Vue.config.productionTip = false; 23 | 24 | // New up the app, passing in the store and router 25 | new Vue({ 26 | router, 27 | render: h => h(App), 28 | store 29 | }).$mount("#app"); 30 | -------------------------------------------------------------------------------- /src/models/IAuthResponse.ts: -------------------------------------------------------------------------------- 1 | interface IUserClaim { 2 | typ: string; 3 | val: string; 4 | } 5 | 6 | interface IAuthResponse { 7 | provider_name: string; 8 | user_claims: Array; 9 | user_id: string; 10 | } 11 | 12 | export { IAuthResponse, IUserClaim }; 13 | -------------------------------------------------------------------------------- /src/models/ILink.ts: -------------------------------------------------------------------------------- 1 | export default interface ILink { 2 | id: string; 3 | url: string; 4 | description: string; 5 | title: string; 6 | image: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/models/IListResponse.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/models/IListResponse.ts -------------------------------------------------------------------------------- /src/models/IOGData.ts: -------------------------------------------------------------------------------- 1 | interface IOGData { 2 | title: string; 3 | description: string; 4 | image: string; 5 | } 6 | 7 | export default IOGData; 8 | -------------------------------------------------------------------------------- /src/models/IUserList.ts: -------------------------------------------------------------------------------- 1 | export default interface IUserList { 2 | vanityUrl: string; 3 | description: string; 4 | linkCount: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/models/Link.ts: -------------------------------------------------------------------------------- 1 | import ILink from "./ILink"; 2 | import cuid from "cuid"; 3 | 4 | export default class Link implements ILink { 5 | constructor( 6 | public id: string = cuid(), 7 | public url: string = "", 8 | public title: string = "", 9 | public description: string = "", 10 | public image: string = "" 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /src/models/List.ts: -------------------------------------------------------------------------------- 1 | import ILink from "./ILink"; 2 | 3 | export default class List { 4 | constructor( 5 | public vanityUrl: string = "", 6 | public description: string = "", 7 | public links: Array = new Array() 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import { IAuthResponse } from "@/models/IAuthResponse"; 2 | 3 | export default class User { 4 | userName: string = ""; 5 | name: string = "Login / Sign Up"; 6 | loggedIn: boolean = false; 7 | profileImage: string = ""; 8 | 9 | constructor(response?: IAuthResponse) { 10 | if (response) { 11 | this.loggedIn = true; 12 | // store just the username part of the email address 13 | this.userName = response.user_id.split("@")[0]; 14 | 15 | for (let claim of response.user_claims) { 16 | if (claim.typ.indexOf("/identity/claims/name") > 0) { 17 | // store just the username part of the email address 18 | this.name = claim.val; 19 | } 20 | 21 | if (claim.typ.indexOf("profile_image_url_https") > 0) { 22 | this.profileImage = claim.val; 23 | } 24 | 25 | if (claim.typ.indexOf("urn:github:avatar_url") > 0) { 26 | this.profileImage = claim.val; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/references.d.ts: -------------------------------------------------------------------------------- 1 | // references.d.ts 2 | /** 3 | * Extends interfaces in Vue.js 4 | */ 5 | 6 | import Vue, { ComponentOptions } from "vue"; 7 | 8 | declare module "vue/types/options" { 9 | interface ComponentOptions { 10 | SlickList?: string; 11 | SlickItem?: string; 12 | } 13 | } 14 | 15 | declare global { 16 | const appInsights: any; 17 | } 18 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router, { Route } from "vue-router"; 3 | import Home from "@/views/Home.vue"; 4 | import List from "@/views/List.vue"; 5 | import User from "@/views/User.vue"; 6 | import Edit from "@/views/Edit.vue"; 7 | 8 | Vue.use(Router); 9 | 10 | let router = new Router({ 11 | mode: "history", 12 | base: process.env.BASE_URL, 13 | routes: [ 14 | { 15 | path: "/", 16 | name: "home", 17 | component: Home, 18 | beforeEnter: (to, from, next) => { 19 | if (to.query.list) { 20 | next({ path: `/${to.query.list}` }); 21 | } else { 22 | next(); 23 | } 24 | } 25 | }, 26 | { 27 | path: "/s/user", 28 | name: "user", 29 | component: User 30 | }, 31 | { 32 | path: "/s/edit", 33 | name: "edit", 34 | component: Edit 35 | }, 36 | { 37 | path: "/:id", 38 | name: "list", 39 | component: List 40 | } 41 | // { 42 | // path: "/:id", 43 | // name: "list", 44 | // // route level code-splitting 45 | // // this generates a separate chunk (about.[hash].js) for this route 46 | // // which is lazy-loaded when the route is visited. 47 | // component: () => 48 | // // import(/* webpackChunkName: "about" */ "./views/About.vue") 49 | // } 50 | ] 51 | }); 52 | 53 | router.beforeEach((to: Route, from: Route, next: any) => { 54 | appInsights.trackPageView(to.fullPath); 55 | next(); 56 | }); 57 | 58 | export default router; 59 | -------------------------------------------------------------------------------- /src/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; 2 | import config from "../config"; 3 | import store from "../store/store"; 4 | 5 | /* 6 | This file overrides the standard axios configuration so that every request has 7 | the correct headers. 8 | 9 | It also attaches event listeners to requests as they go 10 | out, and as they come back. This is how the app knows whether or not to display 11 | the green bar that goes across the top to show activity. 12 | 13 | This file is imported and the init() is called in main.ts 14 | */ 15 | 16 | const ApiService = { 17 | init() { 18 | axios.defaults.headers.common["x-functions-key"] = config.FUNCTION_KEY; 19 | axios.defaults.withCredentials = false; 20 | 21 | axios.interceptors.request.use((config: AxiosRequestConfig) => { 22 | store.dispatch("setAppBusy", true); 23 | return config; 24 | }); 25 | 26 | axios.interceptors.response.use( 27 | (response: AxiosResponse) => { 28 | store.dispatch("setAppBusy", false); 29 | return response; 30 | }, 31 | err => { 32 | store.dispatch("setAppBusy", false); 33 | } 34 | ); 35 | }, 36 | 37 | get(resource: string, requestConfig?: AxiosRequestConfig) { 38 | return axios.get(`${resource}`, requestConfig); 39 | }, 40 | 41 | post(resource: string, data?: any, requestConfig?: AxiosRequestConfig) { 42 | return axios.post(`${resource}`, data, requestConfig); 43 | }, 44 | 45 | patch(resource: string, data?: any, requestConfig?: AxiosRequestConfig) { 46 | return axios.patch(`${resource}`, data, requestConfig); 47 | }, 48 | 49 | destroy(resource: string, requestConfig?: AxiosRequestConfig) { 50 | return axios.delete(`${resource}`, requestConfig); 51 | } 52 | }; 53 | 54 | export default ApiService; 55 | -------------------------------------------------------------------------------- /src/services/list.service.ts: -------------------------------------------------------------------------------- 1 | import ApiService from "./api.service"; 2 | import ILink from "@/models/ILink"; 3 | import IOGData from "@/models/IOGData"; 4 | import Link from "@/models/Link"; 5 | import List from "@/models/List"; 6 | import config from "@/config"; 7 | 8 | const ListService = { 9 | async get(vanityUrl: string): Promise { 10 | const response = await ApiService.get( 11 | `${config.BACKEND}/api/links/${vanityUrl}` 12 | ); 13 | return new List(vanityUrl, response.data.description, >( 14 | response.data.links 15 | )); 16 | }, 17 | async create(payload: object): Promise { 18 | const response = await ApiService.post( 19 | `${config.BACKEND}/api/links`, 20 | payload 21 | ); 22 | return response.data.vanityUrl; 23 | }, 24 | async validate(url: string, id: string): Promise { 25 | const response = await ApiService.post( 26 | `${config.BACKEND}/api/validatePage`, 27 | { 28 | url: url, 29 | id: id 30 | } 31 | ); 32 | const ogData = response.data; 33 | 34 | return new Link( 35 | id, 36 | url, 37 | ogData.title, 38 | ogData.description, 39 | ogData.image ? ogData.image.replace(/(^\w+:|^)/, "") : "" 40 | ); 41 | }, 42 | update(vanityUrl: string, payload: object) { 43 | return ApiService.patch( 44 | `${config.BACKEND}/api/links/${vanityUrl}`, 45 | payload 46 | ); 47 | }, 48 | destroy(vanityUrl: string) { 49 | return ApiService.destroy(`${config.BACKEND}/api/links/${vanityUrl}`); 50 | } 51 | }; 52 | 53 | export default ListService; 54 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import ApiService from "./api.service"; 2 | import User from "@/models/User"; 3 | import IUserList from "@/models/IUserList"; 4 | import config from "@/config"; 5 | 6 | const UserService = { 7 | async me(): Promise { 8 | const response = await ApiService.get(`${config.BACKEND}/.auth/me`); 9 | return new User(response.data[0]); 10 | }, 11 | 12 | async lists(userName: string): Promise> { 13 | const response = await ApiService.get( 14 | `${config.BACKEND}/api/links/user/${userName}` 15 | ); 16 | return response ? >response.data : []; 17 | } 18 | }; 19 | 20 | export default UserService; 21 | -------------------------------------------------------------------------------- /src/shared/Array.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface Array { 5 | get(field: string, value: string): IGetResult; 6 | } 7 | } 8 | 9 | interface IGetResult { 10 | item: object; 11 | index: number; 12 | } 13 | 14 | Array.prototype.get = function(field: string, value: string) { 15 | let index = this.findIndex(x => x[field] === value); 16 | return { item: this[index], index: index }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/store/AppModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, Mutation, Action, VuexModule } from "vuex-module-decorators"; 2 | @Module 3 | export default class AppModule extends VuexModule { 4 | _appIsBusy: boolean = false; 5 | _appErrorMessage: string = ""; 6 | _activeProcesses: number = 0; 7 | 8 | get appIsBusy() { 9 | return this._appIsBusy; 10 | } 11 | 12 | get appErrorMessage() { 13 | return this._appErrorMessage; 14 | } 15 | 16 | get activeProcesses() { 17 | return this._activeProcesses; 18 | } 19 | 20 | @Mutation 21 | _setActiveProcesses(incrementer: number) { 22 | this._activeProcesses += incrementer; 23 | } 24 | 25 | @Mutation 26 | _setAppBusy(busy: boolean) { 27 | this._appIsBusy = busy; 28 | } 29 | 30 | @Action 31 | setAppBusy(busy: boolean) { 32 | let incrementer = busy || -1; 33 | this.context.commit("_setActiveProcesses", incrementer); 34 | this.context.commit("_setAppBusy", this._activeProcesses > 0); 35 | } 36 | 37 | @Mutation 38 | _setAppErrorMessage(message: string) { 39 | this._appErrorMessage = message; 40 | } 41 | 42 | @Action 43 | setAppErrorMessage(message: string) { 44 | this.context.commit("_setAppErrorMessage", message); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/store/ListModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, Mutation, Action, VuexModule } from "vuex-module-decorators"; 2 | import List from "@/models/List"; 3 | import ListService from "@/services/list.service"; 4 | import Link from "@/models/Link"; 5 | import ILink from "@/models/ILink"; 6 | import IUserList from "@/models/IUserList"; 7 | 8 | @Module 9 | export default class ListModule extends VuexModule { 10 | _currentList: List = new List(); 11 | _usersLists: Array = []; 12 | _listIsPublished: boolean = false; 13 | 14 | get currentList() { 15 | return this._currentList; 16 | } 17 | 18 | get usersLists() { 19 | return this._usersLists; 20 | } 21 | 22 | get userOwnsList() { 23 | return ( 24 | this._usersLists.get("vanityUrl", this._currentList.vanityUrl).index > -1 25 | ); 26 | } 27 | 28 | get listIsPublished() { 29 | return this._listIsPublished; 30 | } 31 | 32 | /** 33 | * Mutations 34 | * All mutations are denoted by a "_" modifier 35 | ] */ 36 | 37 | @Mutation 38 | _updateCurrentList(list: List) { 39 | this._currentList = list; 40 | } 41 | 42 | @Mutation 43 | _resetCurrentList() { 44 | this._currentList = new List(); 45 | this._listIsPublished = false; 46 | } 47 | 48 | @Mutation 49 | _updateUsersLists(lists: Array) { 50 | this._usersLists = lists; 51 | } 52 | 53 | @Mutation 54 | _clearUsersLists() { 55 | this._usersLists = []; 56 | } 57 | 58 | @Mutation 59 | _updatevanityUrl(vanityUrl: string) { 60 | this._currentList.vanityUrl = vanityUrl; 61 | } 62 | 63 | @Mutation 64 | _addLink(link: ILink) { 65 | this._currentList.links.push(link); 66 | } 67 | 68 | @Mutation 69 | _updateLink(link: ILink) { 70 | const { index } = this._currentList.links.get("id", link.id); 71 | this._currentList.links.splice(index, 1, link); 72 | } 73 | 74 | @Mutation 75 | _deleteLink(id: string) { 76 | let { index } = this._currentList.links.get("id", id); 77 | this._currentList.links.splice(index, 1); 78 | } 79 | 80 | @Mutation 81 | _setListPublished() { 82 | this._listIsPublished = true; 83 | } 84 | 85 | /** 86 | * Actions 87 | */ 88 | 89 | @Action 90 | updatevanityUrl(vanityUrl: string) { 91 | this.context.commit("_updatevanityUrl", vanityUrl); 92 | } 93 | 94 | @Action 95 | addLink(link: ILink = new Link()) { 96 | this.context.commit("_addLink", link); 97 | this.context.dispatch("updateLink", link); 98 | } 99 | 100 | @Action 101 | newLink(url: string) { 102 | const link = new Link(url, url); 103 | this.context.dispatch("addLink", link); 104 | } 105 | 106 | @Action 107 | async resetCurrentList() { 108 | this.context.commit("_resetCurrentList"); 109 | } 110 | 111 | @Action 112 | async updateLink(link: ILink) { 113 | try { 114 | const updatedLink = await ListService.validate(link.url, link.id); 115 | this.context.commit("_updateLink", updatedLink); 116 | } catch (err) { 117 | throw new Error(err); 118 | } 119 | } 120 | 121 | /* GET LIST */ 122 | @Action 123 | async getList(vanityUrl: string) { 124 | try { 125 | const list = await ListService.get(vanityUrl); 126 | this.context.commit("_updateCurrentList", list); 127 | 128 | this.context.commit("_setListPublished"); 129 | } catch (err) { 130 | throw new Error(err); 131 | } 132 | } 133 | 134 | // @Action 135 | // async getLinkDetails(list) { 136 | // try { 137 | // // we go through each link and update it with the most 138 | // // current information by calling the API method which 139 | // // pulls out the open graph information 140 | // for (let link of list.links) { 141 | // this.context.dispatch("updateLink", link); 142 | // } 143 | // } 144 | // } 145 | 146 | /* SAVE LIST */ 147 | @Action 148 | async publishList() { 149 | const options = { 150 | links: this._currentList.links, 151 | vanityUrl: this._currentList.vanityUrl, 152 | description: this._currentList.description, 153 | userId: this.context.getters.currentUser.userName 154 | }; 155 | 156 | try { 157 | const vanityUrl = await ListService.create(options); 158 | this.context.commit("_updatevanityUrl", vanityUrl); 159 | this.context.commit("_setListPublished"); 160 | } catch (err) { 161 | throw new Error(err); 162 | } 163 | } 164 | 165 | /* UPDATE LIST */ 166 | @Action 167 | async updateList(vanityUrl: string) { 168 | const options = [ 169 | { 170 | op: "replace", 171 | path: "/links", 172 | value: this._currentList.links 173 | }, 174 | { 175 | op: "replace", 176 | path: "/description", 177 | value: this._currentList.description 178 | } 179 | ]; 180 | 181 | try { 182 | await ListService.update(vanityUrl, options); 183 | } catch (err) { 184 | throw new Error(err); 185 | } 186 | } 187 | 188 | @Action 189 | async deleteList(vanityUrl: string) { 190 | try { 191 | await ListService.destroy(vanityUrl); 192 | this.context.commit("_deleteFromUsersLists", vanityUrl); 193 | } catch (err) { 194 | throw new Error(err); 195 | } 196 | } 197 | 198 | @Action 199 | deleteLink(id: string) { 200 | this.context.commit("_deleteLink", id); 201 | } 202 | 203 | /** 204 | * This method checks for the availability of the vanityUrl in the database. It does this by 205 | * just requesting a list by vanityUrl. If a list is returned, the vanityUrl is not available. 206 | * @param vanityUrl The vanityUrl to check for availablility 207 | */ 208 | @Action 209 | async checkvanityUrlAvailable(vanityUrl: string) { 210 | try { 211 | let list = await ListService.get(vanityUrl); 212 | return false; 213 | } catch (err) { 214 | return true; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/store/UserModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, Mutation, Action, VuexModule } from "vuex-module-decorators"; 2 | import User from "../models/User"; 3 | import IUserList from "@/models/IUserList"; 4 | import UserService from "@/services/user.service"; 5 | 6 | @Module 7 | export default class UserModule extends VuexModule { 8 | _currentUser: User = new User(); 9 | _showProfileMenu: boolean = false; 10 | _usersLists: Array = []; 11 | 12 | get currentUser() { 13 | return this._currentUser; 14 | } 15 | 16 | get showProfileMenu() { 17 | return this._showProfileMenu; 18 | } 19 | 20 | /** 21 | * Mutations 22 | * All mutations are denoted by a "_" modifier 23 | */ 24 | 25 | @Mutation 26 | _updateCurrentUser(user: User) { 27 | this._currentUser = user; 28 | } 29 | 30 | @Mutation 31 | _updateUsersLists(usersLists: Array) { 32 | this._usersLists = usersLists; 33 | } 34 | 35 | @Mutation 36 | _toggleProfileMenu() { 37 | this._showProfileMenu = !this._showProfileMenu; 38 | } 39 | 40 | /** 41 | * Actions 42 | */ 43 | 44 | @Action 45 | async getUser() { 46 | try { 47 | const user = await UserService.me(); 48 | this.context.commit("_updateCurrentUser", user); 49 | this.context.dispatch("getUsersLists"); 50 | } catch (err) { 51 | console.log("User is not logged in"); 52 | } 53 | } 54 | 55 | @Action({ rawError: true }) 56 | async getUsersLists() { 57 | if (this.currentUser.loggedIn) { 58 | try { 59 | const userLists = await UserService.lists(this.currentUser.userName); 60 | this.context.commit("_updateUsersLists", userLists); 61 | } catch (err) { 62 | throw new Error(err); 63 | } 64 | } 65 | } 66 | 67 | @Action({ commit: "_toggleProfileMenu" }) 68 | toggleProfileMenu() {} 69 | } 70 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import ListModule from "@/store/ListModule"; 4 | import UserModule from "@/store/UserModule"; 5 | import AppModule from "@/store/AppModule"; 6 | 7 | Vue.use(Vuex); 8 | 9 | const store = new Vuex.Store({ 10 | state: {}, 11 | modules: { 12 | ListModule, 13 | UserModule, 14 | AppModule 15 | } 16 | }); 17 | 18 | export default store; 19 | -------------------------------------------------------------------------------- /src/styles/bulma-custom.scss: -------------------------------------------------------------------------------- 1 | @import "./variables.scss"; 2 | 3 | /* UTILTIES */ 4 | @import "bulma/sass/utilities/animations.sass"; 5 | @import "bulma/sass/utilities/controls.sass"; 6 | @import "bulma/sass/utilities/mixins.sass"; 7 | 8 | /* BASE */ 9 | @import "bulma/sass/base/_all.sass"; 10 | 11 | /* FORM */ 12 | @import "bulma/sass/form/shared.sass"; 13 | @import "bulma/sass/form/input-textarea.sass"; 14 | 15 | /* ELEMENTS */ 16 | @import "bulma/sass/elements/button.sass"; 17 | @import "bulma/sass/elements/container.sass"; 18 | @import "bulma/sass/elements/icon.sass"; 19 | @import "bulma/sass/elements/image.sass"; 20 | @import "bulma/sass/elements/tag.sass"; 21 | 22 | /* LAYOUT */ 23 | @import "bulma/sass/layout/section.sass"; 24 | @import "bulma/sass/layout/hero.sass"; 25 | 26 | /* COLUMNS */ 27 | @import "bulma/sass/grid/columns.sass"; 28 | 29 | /* COMPONENTS */ 30 | @import "bulma/sass/components/card.sass"; 31 | @import "bulma/sass/components/modal.sass"; 32 | @import "bulma/sass/components/navbar.sass"; 33 | -------------------------------------------------------------------------------- /src/styles/site.scss: -------------------------------------------------------------------------------- 1 | @import url("https://use.fontawesome.com/releases/v5.6.3/css/all.css"); 2 | @import "./bulma-custom.scss"; 3 | 4 | .beta-bump { 5 | margin-top: 10px; 6 | } 7 | 8 | html, 9 | body { 10 | height: 100%; 11 | text-rendering: auto; 12 | text-size-adjust: 100%; 13 | background-color: #f9fafc; 14 | } 15 | 16 | body { 17 | padding: 0; 18 | margin: 0; 19 | font-family: "Roboto", sans-serif; 20 | color: #222c38; 21 | font-size: 18px; 22 | } 23 | 24 | .input, 25 | .textarea { 26 | outline: none; 27 | padding: 0px 10px; 28 | height: 3.5rem; 29 | border-radius: 4px; 30 | border: 1px solid transparent; 31 | border-color: #979797; 32 | line-height: 1.5; 33 | width: 100%; 34 | -webkit-box-sizing: border-box; 35 | box-sizing: border-box; 36 | &.is-invalid { 37 | border-width: 4px; 38 | border-color: $danger; 39 | -webkit-animation-name: shakeError; 40 | animation-name: shakeError; 41 | -webkit-animation-fill-mode: forward; 42 | animation-fill-mode: forward; 43 | -webkit-animation-duration: 0.6s; 44 | animation-duration: 0.6s; 45 | -webkit-animation-timing-function: ease-in-out; 46 | animation-timing-function: ease-in-out; 47 | } 48 | } 49 | 50 | .textarea { 51 | &[rows] { 52 | height: 3.5rem; 53 | min-height: auto; 54 | } 55 | &:not([rows]) { 56 | height: auto; 57 | } 58 | } 59 | 60 | a { 61 | color: $primary; 62 | } 63 | 64 | .navbar-burger { 65 | width: inherit; 66 | height: inherit; 67 | } 68 | 69 | .tag:not(body) { 70 | border-radius: 20px; 71 | } 72 | 73 | .main { 74 | max-width: 960px; 75 | padding: 0px 10px; 76 | } 77 | 78 | .section { 79 | padding-top: 0px; 80 | } 81 | 82 | .header { 83 | top: 0; 84 | width: 100%; 85 | background: #fff; 86 | -webkit-box-shadow: 0 5px 40px 1px #e8e8e8; 87 | box-shadow: 0 5px 40px 1px #e8e8e8; 88 | } 89 | 90 | .card { 91 | border-radius: 4px; 92 | box-shadow: 0 5px 40px 1px #e8e8e8; 93 | } 94 | 95 | .content { 96 | margin-top: 100px; 97 | } 98 | 99 | .x { 100 | text-decoration: none; 101 | color: inherit; 102 | } 103 | 104 | .is-heading { 105 | margin: 40px 0; 106 | } 107 | 108 | .navbar-link[data-v-4295d220]:hover { 109 | background-color: white; 110 | color: $primary; 111 | } 112 | 113 | .navbar-link[data-v-4295d220]:not(.is-arrowless)::after { 114 | border-color: $primary; 115 | } 116 | 117 | @keyframes shakeError { 118 | 0% { 119 | transform: translateX(0); 120 | } 121 | 15% { 122 | transform: translateX(0.375rem); 123 | } 124 | 30% { 125 | transform: translateX(-0.375rem); 126 | } 127 | 45% { 128 | transform: translateX(0.375rem); 129 | } 130 | 60% { 131 | transform: translateX(-0.375rem); 132 | } 133 | 75% { 134 | transform: translateX(0.375rem); 135 | } 136 | 90% { 137 | transform: translateX(-0.375rem); 138 | } 139 | 100% { 140 | transform: translateX(0); 141 | } 142 | } 143 | 144 | .errorMessage { 145 | height: 10px; 146 | margin-top: 5px; 147 | } 148 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $green: #20ae96; 2 | $red: #d9255e; 3 | 4 | $primary: $green; 5 | $danger: $red; 6 | 7 | @import "bulma/sass/utilities/initial-variables.sass"; 8 | @import "bulma/sass/utilities/functions.sass"; 9 | @import "bulma/sass/utilities/derived-variables.sass"; 10 | -------------------------------------------------------------------------------- /src/views/Edit.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 80 | 81 | 82 | 91 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 61 | 62 | 96 | -------------------------------------------------------------------------------- /src/views/List.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 143 | 144 | 149 | -------------------------------------------------------------------------------- /src/views/User.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 84 | 85 | 132 | -------------------------------------------------------------------------------- /tests/load-test.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: 'https://linky-link.azurewebsites.net' 3 | phases: 4 | - duration: 60 5 | arrivalRate: 20 6 | defaults: 7 | headers: 8 | x-functions-key: 'SteUyT0o2YSt4JW8k2qZ5ShTBcQYQj1pxE8cO7oTrYI4ps92/53o9w==' 9 | scenarios: 10 | - flow: 11 | - get: 12 | url: "/api/links/cog-services" 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | loaderOptions: { 4 | sass: { 5 | data: `@import "@/styles/variables.scss";` 6 | } 7 | } 8 | }, 9 | configureWebpack: { 10 | devtool: "source-map" 11 | } 12 | }; 13 | --------------------------------------------------------------------------------