├── .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 | [](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 | 
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 |
2 |
9 |
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 |
--------------------------------------------------------------------------------
/src/assets/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-urlist/frontend-vue-typescript/5a032d00d2ffa7cd3b37feb79b908eb2003eda94/src/assets/close.png
--------------------------------------------------------------------------------
/src/assets/close.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/LinkList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
52 |
--------------------------------------------------------------------------------
/src/components/LinkListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
36 |
37 |
38 |
45 |
46 |
47 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
80 |
81 |
82 |
83 |
84 |
85 |
115 |
116 |
117 |
209 |
--------------------------------------------------------------------------------
/src/components/ListDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
41 |
42 |
43 |
50 |
51 |
52 |
53 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
155 |
156 |
157 |
187 |
--------------------------------------------------------------------------------
/src/components/ListForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Links
8 |
9 |
10 |
11 | Drag links to re-order
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
86 |
87 |
88 |
97 |
--------------------------------------------------------------------------------
/src/components/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
33 |
34 |
35 |
56 |
--------------------------------------------------------------------------------
/src/components/ModalDelete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Delete this list?
4 |
5 |
6 | The url
7 | {{ currentList.vanityUrl }} will be
8 | released for others to use.
9 |
10 |
11 |
18 |
19 |
20 |
21 |
53 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/src/components/ModalLogin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sign in to
7 |
8 |
9 |
10 |

11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
42 |
43 |
44 |
53 |
--------------------------------------------------------------------------------
/src/components/NavBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
105 |
106 |
107 |
146 |
147 |
148 |
189 |
--------------------------------------------------------------------------------
/src/components/NewLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Enter a link and press enter
4 |
14 |
15 |
16 | That doesn't look like a valid URL
17 |
18 |
19 |
20 |
21 |
22 |
58 |
--------------------------------------------------------------------------------
/src/components/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ¯\_
7 | (ツ)
8 | _/¯
9 |
10 |
11 |
12 | We couldn't find that Url List.
13 |
14 |
15 |
16 | But that's not necessarily a bad thing. That means the url "{{
17 | vanityUrl
18 | }}" is
19 | still available.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
43 |
44 |
45 |
78 |
--------------------------------------------------------------------------------
/src/components/Notification.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | Make sure you enter a valid URL
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 |
54 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
19 |
20 |
21 |
86 |
--------------------------------------------------------------------------------
/src/components/QrCode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/UserMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Links
10 |
11 |
12 |
13 | Drag links to re-order
14 |
15 |
16 |
17 |
25 |
26 |
30 |
31 |
32 |
33 |
80 |
81 |
82 |
91 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Group links,
10 | Save &
11 | Share them with the world
12 |
13 |
14 |
Add links to a list and share it with one simple URL.
15 |
16 |
17 | Create a list anonymously or login to save, manage, and edit
18 | your lists.
19 |
20 |
21 |
22 |
23 |

24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 | Get Started
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
61 |
62 |
96 |
--------------------------------------------------------------------------------
/src/views/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 | {{ currentList.description }}
9 |
10 |
11 |
52 |
53 |
58 |
66 |
67 |
75 |
76 |
77 |
78 |
79 |
84 |
85 |
86 |
87 |
88 |
89 |
94 |
95 |
96 |
97 |
98 |
99 |
143 |
144 |
149 |
--------------------------------------------------------------------------------
/src/views/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | My Collections
6 |
7 |
8 |
11 |
12 |
13 |
14 |
+
15 |
Create new collection
16 |
17 |
18 |
19 |
20 |
25 |

30 |
34 |
37 | {{ list.linkCount }} Links
38 |
39 |
40 |
41 |
44 | {{ list.vanityUrl }}
45 |
46 |
{{ list.description }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
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 |
--------------------------------------------------------------------------------