├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── babel.config.js
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── img
│ └── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── android-chrome-maskable-192x192.png
│ │ ├── android-chrome-maskable-512x512.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-60x60.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── msapplication-icon-144x144.png
│ │ ├── mstile-150x150.png
│ │ └── safari-pinned-tab.svg
├── index.html
└── robots.txt
├── screenshot.jpg
├── src
├── App.vue
├── assets
│ ├── logo.png
│ └── styles
│ │ ├── fonts.css
│ │ └── tailwind.css
├── axios.ts
├── components
│ ├── atoms
│ │ ├── Alert.vue
│ │ ├── BaseButton.vue
│ │ ├── BaseInput.vue
│ │ ├── BaseSelect.vue
│ │ ├── BigButton.vue
│ │ ├── Container.vue
│ │ ├── Icon.vue
│ │ └── Spinner.vue
│ ├── molecules
│ │ ├── AdminListItem.vue
│ │ ├── CartItem.vue
│ │ ├── Hero.vue
│ │ ├── Modal.vue
│ │ ├── MoreButton.vue
│ │ ├── ProductCard.vue
│ │ └── Toastie.vue
│ ├── organisms
│ │ ├── BaseFooter.vue
│ │ ├── Drawer.vue
│ │ ├── Navigation.vue
│ │ └── ProductGrid.vue
│ └── popups
│ │ ├── LoginForm.vue
│ │ ├── RegisterForm.vue
│ │ └── UserPopup.vue
├── main.ts
├── model
│ └── State.ts
├── registerServiceWorker.ts
├── router
│ ├── index.ts
│ └── router.d.ts
├── shims-vue.d.ts
├── shims-vuex.d.ts
├── store
│ ├── actions.ts
│ ├── getters.ts
│ ├── index.ts
│ └── mutations.ts
└── views
│ ├── About.vue
│ ├── AddProduct.vue
│ ├── AllProducts.vue
│ ├── Cart.vue
│ ├── Dashboard.vue
│ ├── Error.vue
│ ├── Favourites.vue
│ ├── Home.vue
│ ├── Product.vue
│ ├── Profile.vue
│ └── Settings.vue
├── tailwind.config.js
├── tsconfig.json
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: [
7 | "plugin:vue/vue3-essential",
8 | "eslint:recommended",
9 | "@vue/typescript/recommended",
10 | "@vue/prettier",
11 | "@vue/prettier/@typescript-eslint",
12 | "prettier",
13 | ],
14 | parserOptions: {
15 | ecmaVersion: 2020,
16 | },
17 | rules: {
18 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
19 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
20 | "@typescript-eslint/camelcase": "off",
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | .env
5 |
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | pnpm-debug.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Shuuz
2 |
3 | shoe e-commerce website
4 |
5 |
6 |
7 | ## Features
8 |
9 | - Own design
10 | - Vue3 + Vuex store
11 | - Strapi as a back-end
12 | - JWT Auth
13 | - Products can be added, edited and deleted by admin user.
14 | - Hero banner can be customized in admin dashboard.
15 | - Customer can store favourite items.
16 | - Cart shows all added items and sums up the total price
17 |
18 | ## Credits
19 |
20 | - [
TailwindCSS](https://tailwindcss.com/)
21 | - [
TypeScript](https://www.typescriptlang.org/)
22 | - [
Vue 3](https://v3.vuejs.org/)
23 | - [
Vuex 4](https://vuex.vuejs.org/)
24 | - [
Strapi](https://strapi.io/)
25 | - [Eva-Icons](https://akveo.github.io/eva-icons/#/)
26 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/cli-plugin-babel/preset"]
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "http://bartektelec.github.io/shuuz-client",
3 | "name": "client",
4 | "version": "0.1.0",
5 | "private": true,
6 | "scripts": {
7 | "predeploy": "yarn build",
8 | "deploy": "gh-pages -d dist",
9 | "serve": "vue-cli-service serve",
10 | "build": "vue-cli-service build",
11 | "lint": "vue-cli-service lint"
12 | },
13 | "dependencies": {
14 | "core-js": "^3.6.5",
15 | "eva-icons": "^1.1.3",
16 | "gh-pages": "^3.1.0",
17 | "register-service-worker": "^1.7.1",
18 | "vue": "^3.0.0",
19 | "vue-router": "^4.0.0-0",
20 | "vuex": "^4.0.0-0"
21 | },
22 | "devDependencies": {
23 | "@typescript-eslint/eslint-plugin": "^2.33.0",
24 | "@typescript-eslint/parser": "^2.33.0",
25 | "@vue/cli-plugin-babel": "~4.5.0",
26 | "@vue/cli-plugin-eslint": "~4.5.0",
27 | "@vue/cli-plugin-pwa": "~4.5.0",
28 | "@vue/cli-plugin-router": "~4.5.0",
29 | "@vue/cli-plugin-typescript": "~4.5.0",
30 | "@vue/cli-plugin-vuex": "~4.5.0",
31 | "@vue/cli-service": "~4.5.0",
32 | "@vue/compiler-sfc": "^3.0.0",
33 | "@vue/eslint-config-prettier": "^6.0.0",
34 | "@vue/eslint-config-typescript": "^5.0.2",
35 | "axios": "^0.18.0",
36 | "eslint": "^6.7.2",
37 | "eslint-plugin-prettier": "^3.1.3",
38 | "eslint-plugin-vue": "^7.0.0-0",
39 | "prettier": "^1.19.1",
40 | "sass": "^1.26.5",
41 | "sass-loader": "^8.0.2",
42 | "typescript": "~3.9.3",
43 | "vue-cli-plugin-axios": "~0.0.4",
44 | "vue-cli-plugin-tailwind": "~2.0.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-maskable-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/android-chrome-maskable-192x192.png
--------------------------------------------------------------------------------
/public/img/icons/android-chrome-maskable-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/android-chrome-maskable-512x512.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/public/img/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/img/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/icons/msapplication-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/msapplication-icon-144x144.png
--------------------------------------------------------------------------------
/public/img/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/public/img/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/img/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 |
12 |
13 | <%= htmlWebpackPlugin.options.title %>
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/screenshot.jpg
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.message }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
53 |
54 |
102 |
103 |
113 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartektelec/shuuz-client/9645f487d58acf44d36437ee8f34f4dd8c8a9b36/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/styles/fonts.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Princess+Sofia&display=swap");
2 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@700&display=swap");
3 | @import url("https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;700&display=swap");
4 |
--------------------------------------------------------------------------------
/src/assets/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/axios.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | axios.defaults.baseURL = process.env.VUE_APP_API_URL;
4 |
--------------------------------------------------------------------------------
/src/components/atoms/Alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
29 |
30 |
36 |
68 |
--------------------------------------------------------------------------------
/src/components/atoms/BaseButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
37 |
38 |
52 |
53 |
107 |
--------------------------------------------------------------------------------
/src/components/atoms/BaseInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
13 |
21 |
22 | {{ info }}
23 |
24 |
25 |
26 |
27 |
53 |
54 |
104 |
--------------------------------------------------------------------------------
/src/components/atoms/BaseSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
13 |
28 |
{{ info }}
29 |
30 |
31 |
32 |
58 |
59 |
113 |
--------------------------------------------------------------------------------
/src/components/atoms/BigButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
28 |
--------------------------------------------------------------------------------
/src/components/atoms/Container.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
31 |
--------------------------------------------------------------------------------
/src/components/atoms/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 |
18 |
72 |
--------------------------------------------------------------------------------
/src/components/atoms/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Loading content...
4 |
5 |
6 |
7 |
17 |
18 |
48 |
--------------------------------------------------------------------------------
/src/components/molecules/AdminListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Item title
5 |
6 |
11 | Edit product
13 |
14 | Delete product
15 |
16 |
17 |
18 |
19 | Edit product
20 | {{ item }}
21 |
22 |
23 |
24 |
25 |
48 |
49 |
58 |
--------------------------------------------------------------------------------
/src/components/molecules/CartItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | Picture of {{ itemData.brand }} {{ itemData.title }}
14 |
15 |
16 |
17 |
{{ itemData.title }}
18 |
{{ itemData.brand }}
19 |
${{ itemData.price }}
20 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
56 |
57 |
84 |
--------------------------------------------------------------------------------
/src/components/molecules/Hero.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | {{ hero.hero_title }}
12 | {{ hero.hero_text }}
13 |
14 |
15 | {{ hero.hero_btn_text }}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
38 |
39 |
62 |
68 |
--------------------------------------------------------------------------------
/src/components/molecules/Modal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.message }}
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
31 |
32 |
66 |
67 |
72 |
--------------------------------------------------------------------------------
/src/components/molecules/MoreButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/components/molecules/ProductCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
24 |
29 |
30 |
31 | Delete
34 |
35 |
36 | Cancel
39 | Confirm
44 |
45 |
46 |
47 |
48 |
49 |
50 |
85 |
86 |
100 |
101 |
117 |
--------------------------------------------------------------------------------
/src/components/molecules/Toastie.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
19 |
62 |
63 |
113 |
--------------------------------------------------------------------------------
/src/components/organisms/BaseFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 | //
27 |
44 |
45 |
53 |
69 |
--------------------------------------------------------------------------------
/src/components/organisms/Drawer.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
32 |
44 |
45 |
88 |
--------------------------------------------------------------------------------
/src/components/organisms/Navigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | Shuuz
12 |
34 |
35 |
42 |
43 |
49 | Shopping cart
50 |
51 |
52 |
59 | Menu
60 |
61 |
62 |
63 |
64 |
65 |
66 |
96 |
97 |
137 |
--------------------------------------------------------------------------------
/src/components/organisms/ProductGrid.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ title }}
3 |
4 |
12 | {{
13 | btnText
14 | }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
45 |
46 |
55 |
--------------------------------------------------------------------------------
/src/components/popups/LoginForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
59 |
60 |
65 |
--------------------------------------------------------------------------------
/src/components/popups/RegisterForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
51 |
52 |
53 |
139 |
140 |
145 |
--------------------------------------------------------------------------------
/src/components/popups/UserPopup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
Log in
19 |
Don't have an account yet?
20 |
Register
27 |
28 |
33 | Admin dashboard
36 |
38 |
39 | My favourites
41 |
42 |
43 | Log out
49 |
50 |
51 |
52 |
53 |
54 |
84 |
85 |
99 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import "./axios";
3 | import App from "./App.vue";
4 | import "./registerServiceWorker";
5 | import router from "./router";
6 | import { store } from "./store";
7 | import "./assets/styles/tailwind.css";
8 | import "./assets/styles/fonts.css";
9 |
10 | import Spinner from "@/components/atoms/Spinner.vue";
11 | import Alert from "@/components/atoms/Alert.vue";
12 | import BaseSelect from "@/components/atoms/BaseSelect.vue";
13 | import BaseButton from "@/components/atoms/BaseButton.vue";
14 | import BaseInput from "@/components/atoms/BaseInput.vue";
15 | import Container from "@/components/atoms/Container.vue";
16 | import Icon from "@/components/atoms/Icon.vue";
17 |
18 | import Modal from "@/components/molecules/Modal.vue";
19 | import Toastie from "@/components/molecules/Toastie.vue";
20 |
21 | createApp(App)
22 | .component("Spinner", Spinner)
23 | .component("Toastie", Toastie)
24 | .component("Modal", Modal)
25 | .component("Alert", Alert)
26 | .component("BaseSelect", BaseSelect)
27 | .component("BaseButton", BaseButton)
28 | .component("BaseInput", BaseInput)
29 | .component("Container", Container)
30 | .component("Icon", Icon)
31 | .use(store)
32 | .use(router)
33 | .mount("#app");
34 |
--------------------------------------------------------------------------------
/src/model/State.ts:
--------------------------------------------------------------------------------
1 | export interface ImageFormat {
2 | ext: string;
3 | hash: string;
4 | height: number;
5 | mime: string;
6 | name: string;
7 | path: string | null;
8 | size: number;
9 | url: string;
10 | width: number;
11 | }
12 |
13 | export interface Image {
14 | alternativeText: string;
15 | caption: string;
16 | created_at: string;
17 | ext: string;
18 | formats: {
19 | [key: string]: ImageFormat;
20 | };
21 | hash: string;
22 | height: number;
23 | id: number;
24 | mime: string;
25 | name: string;
26 | previewUrl: null;
27 | provider: string;
28 | provider_metadata: null;
29 | size: number;
30 | updated_at: "2020-11-05T10:59:26.282Z";
31 | url: string;
32 | width: number;
33 | }
34 |
35 | export interface Product {
36 | brand: string;
37 | category: string;
38 | created_at: Date;
39 | description: string;
40 | featured: boolean;
41 | id: number;
42 | image: Image;
43 | image_url: string | null;
44 | price: number;
45 | published_at: string;
46 | rating: number;
47 | title: string;
48 | updated_at: string;
49 | }
50 |
51 | export interface Hero {
52 | created_at: string;
53 | hero_banner: Image;
54 | hero_btn_text: string;
55 | hero_route: string;
56 | hero_text: string;
57 | hero_title: string;
58 | hero_image_url: string;
59 | id: number;
60 | published_at: string;
61 | updated_at: string;
62 | }
63 |
64 | export interface CartItem {
65 | id: number;
66 | amount: number;
67 | }
68 |
69 | export interface User {
70 | blocked: boolean;
71 | confirmed: boolean;
72 | created_at: string;
73 | email: string;
74 | id: number;
75 | provider: string;
76 | role: {
77 | id: number;
78 | name: string;
79 | description: string;
80 | type: string;
81 | };
82 | updated_at: string;
83 | username: string;
84 | }
85 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { register } from "register-service-worker";
4 |
5 | if (process.env.NODE_ENV === "production") {
6 | register(`${process.env.BASE_URL}service-worker.js`, {
7 | ready() {
8 | console.log(
9 | "App is being served from cache by a service worker.\n" +
10 | "For more details, visit https://goo.gl/AFskqB"
11 | );
12 | },
13 | registered() {
14 | console.log("Service worker has been registered.");
15 | },
16 | cached() {
17 | console.log("Content has been cached for offline use.");
18 | },
19 | updatefound() {
20 | console.log("New content is downloading.");
21 | },
22 | updated() {
23 | console.log("New content is available; please refresh.");
24 | },
25 | offline() {
26 | console.log(
27 | "No internet connection found. App is running in offline mode."
28 | );
29 | },
30 | error(error) {
31 | console.error("Error during service worker registration:", error);
32 | }
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
2 | import { store } from "../store/index";
3 | import Home from "../views/Home.vue";
4 |
5 | const routes: Array = [
6 | {
7 | path: "/",
8 | name: "Home",
9 | component: Home
10 | },
11 | {
12 | path: "/about",
13 | name: "About",
14 | // route level code-splitting
15 | // this generates a separate chunk (about.[hash].js) for this route
16 | // which is lazy-loaded when the route is visited.
17 | component: () =>
18 | import(/* webpackChunkName: "about" */ "../views/About.vue")
19 | },
20 | {
21 | path: "/products/:category?",
22 | name: "Products",
23 | component: () =>
24 | import(/* webpackChunkName: "allproducts" */ "../views/AllProducts.vue")
25 | },
26 | {
27 | path: "/product/:id",
28 | name: "Product",
29 | component: () =>
30 | import(/* webpackChunkName: "allproducts" */ "../views/Product.vue")
31 | },
32 | {
33 | path: "/dashboard/:page?/:id?",
34 | name: "Dashboard",
35 | meta: {
36 | requiresAdmin: true
37 | },
38 | component: () =>
39 | import(/* webpackChunkName: "dashboard" */ "../views/Dashboard.vue")
40 | },
41 | {
42 | path: "/cart",
43 | name: "Cart",
44 | component: () =>
45 | import(/* webpackChunkName: "dashboard" */ "../views/Cart.vue")
46 | },
47 | {
48 | path: "/favourites",
49 | name: "Favourites",
50 | component: () =>
51 | import(/* webpackChunkName: "dashboard" */ "../views/Favourites.vue")
52 | },
53 | {
54 | path: "/:pathMatch(.*)*",
55 | name: "Error",
56 | component: () =>
57 | import(/* webpackChunkName: "dashboard" */ "../views/Error.vue")
58 | }
59 | ];
60 |
61 | const router = createRouter({
62 | history: createWebHashHistory(),
63 | routes
64 | });
65 |
66 | router.beforeEach(async (to, from, next) => {
67 | const reqAuth = to.matched.some(record => record.meta.requiresAuth);
68 | const reqAdmin = to.matched.some(record => record.meta.requiresAdmin);
69 |
70 | if (!reqAuth && !reqAdmin) return next();
71 |
72 | if (!store.getters.getJWT) {
73 | store.commit("addToast", {
74 | type: "error",
75 | message: "Auth error: User not logged in"
76 | });
77 |
78 | return next(false);
79 | }
80 | if (reqAdmin) {
81 | try {
82 | const userData = await store.getters.getMe;
83 |
84 | const roleId = await userData.role.id;
85 | if (roleId !== 1) throw new Error("User is not admin");
86 | return next();
87 | } catch (error) {
88 | store.commit("addToast", {
89 | type: "error",
90 | message: "Auth error: " + error.message
91 | });
92 | next(false);
93 | }
94 | }
95 | if (reqAuth) {
96 | try {
97 | const userData = await store.getters.getMe;
98 |
99 | const isConfirmed = await userData.confirmed;
100 | const isBlocked = await userData.blocked;
101 | if (!userData) throw new Error("User not logged in");
102 | if (!isConfirmed) throw new Error("User not confirmed");
103 | if (isBlocked) throw new Error("User is banned");
104 | next();
105 | } catch (error) {
106 | store.commit("addToast", {
107 | type: "error",
108 | message: "Auth error: " + error.message
109 | });
110 | next(false);
111 | }
112 | }
113 | });
114 |
115 | export default router;
116 |
--------------------------------------------------------------------------------
/src/router/router.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
3 | declare module "vue-router" {
4 | interface RouteMeta {
5 | requiresAdmin?: boolean;
6 | requiresAuth?: boolean;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import type { DefineComponent } from 'vue'
3 | const component: DefineComponent<{}, {}, any>
4 | export default component
5 | }
6 |
--------------------------------------------------------------------------------
/src/shims-vuex.d.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "vuex";
2 | import { State } from "./store";
3 |
4 | declare module "@vue/runtime-core" {
5 | interface ComponentCustomProperties {
6 | $store: Store;
7 | }
8 | }
9 |
10 | declare module "vuex" {
11 | export function useStore(key?: string): Store;
12 | }
13 |
--------------------------------------------------------------------------------
/src/store/actions.ts:
--------------------------------------------------------------------------------
1 | import { ActionTree } from "vuex";
2 | import axios from "axios";
3 | import { State } from "./index";
4 | import router from "../router/index";
5 |
6 | const actions: ActionTree = {
7 | async onInit(context) {
8 | context.commit("setCachedJWTToken");
9 | context.dispatch("fetchHeroData");
10 | context.dispatch("fetchAllProducts");
11 | context.commit("setCachedCart");
12 | context.commit("getCachedFavourites");
13 | },
14 |
15 | async fetchAllProducts(context) {
16 | try {
17 | const payload = await axios.get("/products");
18 | context.commit("setProducts", payload.data);
19 | } catch (error) {
20 | context.commit("addToast", {
21 | type: "error",
22 | message:
23 | error.name + ": Unable to fetch the products list, try again later"
24 | });
25 | console.error(error.message);
26 | }
27 | },
28 |
29 | async fetchHeroData(context) {
30 | try {
31 | const payload = await axios.get("/home");
32 | context.commit("setHero", payload.data);
33 | } catch (error) {
34 | console.error(error.message);
35 | }
36 | },
37 |
38 | async setHeroData(context, payload) {
39 | try {
40 | await axios.put("/home", payload, {
41 | headers: {
42 | Authorization: "Bearer " + context.state.jwt
43 | }
44 | });
45 | context.commit("setHero", payload);
46 | context.commit("addToast", {
47 | message: "Hero updated successfully"
48 | });
49 | } catch (error) {
50 | context.commit("addToast", {
51 | type: "error",
52 | message: "Couldn't update hero information"
53 | });
54 | console.error(error.message);
55 | }
56 | },
57 |
58 | async addProduct(context, payload) {
59 | try {
60 | await axios.post("/products", payload, {
61 | headers: {
62 | Authorization: "Bearer " + context.state.jwt
63 | }
64 | });
65 | await context.dispatch("fetchAllProducts");
66 | context.commit("addToast", {
67 | message: "Product has been added"
68 | });
69 | } catch (error) {
70 | context.commit("addToast", {
71 | type: "error",
72 | message: "Couldn't add new product"
73 | });
74 | console.error(error.message);
75 | }
76 | },
77 | async updateProduct(context, payload) {
78 | try {
79 | const { id, ...newProduct } = payload;
80 | console.log(`id: ${id}`);
81 | console.log(`newProduct: ${newProduct}`);
82 | await axios.put("/products/" + id, newProduct, {
83 | headers: {
84 | Authorization: "Bearer " + context.state.jwt
85 | }
86 | });
87 | await context.dispatch("fetchAllProducts");
88 | context.commit("addToast", {
89 | message: "Product has been updated"
90 | });
91 | } catch (error) {
92 | context.commit("addToast", {
93 | type: "error",
94 | message: "Couldn't update product"
95 | });
96 | console.error(error.message);
97 | }
98 | },
99 | async deleteProduct(context, payload) {
100 | try {
101 | await axios.delete("/products/" + payload, {
102 | headers: {
103 | Authorization: "Bearer " + context.state.jwt
104 | }
105 | });
106 | await context.dispatch("fetchAllProducts");
107 | context.commit("addToast", {
108 | message: "Product has been deleted"
109 | });
110 | } catch (error) {
111 | context.commit("addToast", {
112 | type: "error",
113 | message: "Couldn't delete product"
114 | });
115 | console.error(error.message);
116 | }
117 | },
118 |
119 | addFavourite(context, payload) {
120 | if (context.state.favourites.includes(payload)) {
121 | return context.commit("removeFavourite", payload);
122 | }
123 |
124 | context.commit("addFavourite", payload);
125 | context.commit("addToast", { message: "Product added to favourites" });
126 | },
127 | addToCart(context, payload) {
128 | if (context.state.cart.some(item => item.id === payload))
129 | return context.commit("addToast", {
130 | type: "error",
131 | message: "Product already in cart"
132 | });
133 |
134 | context.commit("addToCart", payload);
135 | context.commit("addToast", { message: "Product added to cart" });
136 | },
137 | removeFromCart(context, payload) {
138 | if (context.state.cart.every(item => item.id !== payload)) return;
139 |
140 | context.commit("removeFromCart", payload);
141 | context.commit("addToast", { message: "Product removed from cart" });
142 | },
143 | async authorize(context, payload) {
144 | try {
145 | const user = await axios.post("/auth/local", payload);
146 | if (!user.data.user || !user.data.jwt) return;
147 | context.commit("setJWTToken", user.data.jwt);
148 | context.commit("addToast", {
149 | message: "Successfully logged in as " + user.data.user.username
150 | });
151 | context.commit("setModal", null);
152 | } catch (error) {
153 | context.commit("addToast", {
154 | type: "error",
155 | message: error.name + ": Unable to authorize user."
156 | });
157 | console.error(error.message);
158 | }
159 | },
160 |
161 | async register(context, payload) {
162 | try {
163 | console.log(payload);
164 | await axios.post("/auth/local/register", payload);
165 |
166 | context.commit("addToast", {
167 | message: "Registered successfully"
168 | });
169 | await context.dispatch("authorize", {
170 | identifier: payload.username,
171 | password: payload.password
172 | });
173 | context.commit("setModal", null);
174 | } catch (error) {
175 | context.commit("addToast", {
176 | type: "error",
177 | message: error.name + ":" + error.message
178 | });
179 | console.error(error.message);
180 | }
181 | },
182 |
183 | logout(context) {
184 | context.commit("clearJWTToken", "");
185 | context.commit("addToast", { message: "Logged out successfully" });
186 | router.push("/");
187 | }
188 | };
189 |
190 | export default actions;
191 |
--------------------------------------------------------------------------------
/src/store/getters.ts:
--------------------------------------------------------------------------------
1 | import { GetterTree } from "vuex";
2 | import { State } from "./index";
3 | import { lsKeys } from "./mutations";
4 | import { Product } from "../model/State";
5 | import axios from "axios";
6 |
7 | const getters: GetterTree = {
8 | getAllProducts(state) {
9 | return state.products;
10 | },
11 | getFeaturedProducts(state) {
12 | return state.products.filter(product => product.featured === true);
13 | },
14 |
15 | getProductById: state => (id: string | number) => {
16 | const isNumber = (id: string | number): id is number =>
17 | typeof id === "number";
18 | if (!isNumber(id)) {
19 | id = parseInt(id);
20 | }
21 | const foundProduct =
22 | state.products.find((item: Product) => item.id === id) || null;
23 | return foundProduct;
24 | },
25 | getAllFavourites(state) {
26 | return state.favourites
27 | .map(productId => {
28 | const foundProduct = state.products.find(item => item.id === productId);
29 | if (!foundProduct) return;
30 | return foundProduct;
31 | })
32 | .filter(item => item !== undefined);
33 | },
34 | getRandomProductName(state) {
35 | const rand = Math.floor(Math.random() * state.products.length);
36 | const randProduct = state.products[rand];
37 | return `${randProduct.brand} ${randProduct.title}`;
38 | },
39 |
40 | getJWT(state) {
41 | const cachedJWT = localStorage.getItem(lsKeys.jwt);
42 | if (!state.jwt || !cachedJWT) return null;
43 |
44 | return state.jwt || cachedJWT;
45 | },
46 |
47 | async getMe(state, getters) {
48 | if (!getters.getJWT) return null;
49 |
50 | const userData = await axios.get("/users/me", {
51 | headers: {
52 | Authorization: "Bearer " + getters.getJWT
53 | }
54 | });
55 |
56 | return userData.data;
57 | }
58 | };
59 |
60 | export default getters;
61 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from "vuex";
2 | import { Product, Hero, CartItem } from "../model/State";
3 | import mutations from "./mutations";
4 | import actions from "./actions";
5 | import getters from "./getters";
6 |
7 | export const store = createStore({
8 | state: {
9 | products: [],
10 | cart: [],
11 | jwt: "",
12 | toasts: [],
13 | favourites: [],
14 | hero: null,
15 | modal: null
16 | },
17 | mutations: mutations,
18 | actions: actions,
19 | getters: getters,
20 | modules: {}
21 | });
22 |
23 | interface ErrorItem {
24 | type: "primary" | "error";
25 | icon: string;
26 | message: string;
27 | }
28 |
29 | export type State = {
30 | products: Product[];
31 | cart: CartItem[];
32 | jwt: string;
33 | toasts: ErrorItem[];
34 | favourites: number[];
35 | hero: Hero | null;
36 | modal: string | null;
37 | };
38 |
--------------------------------------------------------------------------------
/src/store/mutations.ts:
--------------------------------------------------------------------------------
1 | import { MutationTree } from "vuex";
2 | import { State } from "./index";
3 |
4 | export const lsKeys = {
5 | favourites: "shuuz-fav",
6 | jwt: "shuuz-token",
7 | cart: "shuuz-basket"
8 | };
9 |
10 | const MAX_AMOUNT_TOASTS = 3;
11 |
12 | const mutations: MutationTree = {
13 | setProducts(state, payload) {
14 | state.products = payload;
15 | },
16 | addToCart(state, payload) {
17 | if (state.cart.some(item => item.id === payload)) return;
18 | const cartItem = { amount: 1, id: payload };
19 | state.cart.push(cartItem);
20 | localStorage.setItem(lsKeys.cart, JSON.stringify(state.cart));
21 | },
22 |
23 | removeFromCart(state, payload) {
24 | const indexToRemove = state.cart.findIndex(item => item.id === payload);
25 | if (indexToRemove < 0) return;
26 | state.cart.splice(indexToRemove, 1);
27 | localStorage.setItem(lsKeys.cart, JSON.stringify(state.cart));
28 | },
29 | setCachedCart(state) {
30 | const cachedCart = localStorage.getItem(lsKeys.cart);
31 | if (!cachedCart) return;
32 | state.cart = JSON.parse(cachedCart);
33 | },
34 | addToast(state, payload) {
35 | if (state.toasts.length >= MAX_AMOUNT_TOASTS) state.toasts.splice(0, 1);
36 | state.toasts.push(payload);
37 | },
38 | setHero(state, payload) {
39 | state.hero = payload;
40 | },
41 | setModal(state, payload) {
42 | state.modal = payload;
43 | },
44 | removeToast(state, payload) {
45 | if (payload >= state.toasts.length) return;
46 |
47 | state.toasts.splice(payload, 1);
48 | },
49 | addFavourite(state, payload) {
50 | if (state.favourites.includes(payload)) return;
51 | state.favourites.push(payload);
52 | localStorage.setItem(lsKeys.favourites, JSON.stringify(state.favourites));
53 | },
54 |
55 | removeFavourite(state, payload) {
56 | const indexToRemove = state.favourites.findIndex(id => id === payload);
57 | if (indexToRemove < 0) return;
58 | state.favourites.splice(indexToRemove, 1);
59 | localStorage.setItem(lsKeys.favourites, JSON.stringify(state.favourites));
60 | },
61 | setCachedJWTToken(state) {
62 | const cachedToken = localStorage.getItem(lsKeys.jwt);
63 | if (!cachedToken) return;
64 | state.jwt = cachedToken;
65 | },
66 | setJWTToken(state, payload) {
67 | state.jwt = payload;
68 | localStorage.setItem(lsKeys.jwt, state.jwt);
69 | },
70 | clearJWTToken(state) {
71 | state.jwt = "";
72 | localStorage.removeItem(lsKeys.jwt);
73 | },
74 | getCachedFavourites(state) {
75 | const cachedFavourites = localStorage.getItem(lsKeys.favourites);
76 | if (!cachedFavourites) return;
77 | state.favourites = JSON.parse(cachedFavourites);
78 | }
79 | };
80 |
81 | export default mutations;
82 |
--------------------------------------------------------------------------------
/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is an about page
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/views/AddProduct.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Product details
4 |
84 |
85 |
86 |
87 |
185 |
186 |
227 |
--------------------------------------------------------------------------------
/src/views/AllProducts.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Products
4 |
5 | Found {{ results.length }} products matching the filters of
6 | {{ items.length }}
7 |
8 |
85 |
86 |
95 |
96 |
97 |
204 |
205 |
227 |
--------------------------------------------------------------------------------
/src/views/Cart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Shopping cart
6 |
{{ $store.state.cart.length || "No" }} products in cart.
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
Products in cart
19 |
${{ totalProducts.toFixed(2) }}
20 |
21 |
22 |
Shipping
23 |
${{ shippingPrice.toFixed(2) }}
24 |
25 |
26 |
27 |
Total
28 |
${{ totalPrice.toFixed(2) }}
29 |
30 |
Proceed to checkout
33 |
34 |
35 |
36 |
37 |
38 |
39 |
63 |
64 |
98 |
133 |
--------------------------------------------------------------------------------
/src/views/Dashboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
56 |
57 |
83 |
--------------------------------------------------------------------------------
/src/views/Error.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Error occured
4 | Something went wrong. This page doesn't exist.
5 | Go back to Home page
6 |
7 |
8 |
9 |
14 |
15 |
20 |
--------------------------------------------------------------------------------
/src/views/Favourites.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | My favourites
4 |
5 | {{ $store.getters.getAllFavourites.length }} products added to favourites.
6 |
7 |
17 |
18 |
19 | Favourites list is empty, add products to favourites by clicking on the
20 | heart icon.
21 |
22 |
Go back
27 |
28 |
29 |
30 |
31 |
42 |
43 |
47 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/views/Product.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Products /
5 | {{
6 | itemData.category
7 | }}
8 |
9 |
10 |
(e.target.style.backgroundPosition = '')"
14 | :style="{
15 | backgroundImage: `url(${itemData.image_url ||
16 | api + itemData.image.url}`,
17 | }"
18 | >
19 |
20 |
21 |
{{ itemData.title }}
22 |
{{ itemData.brand }}
23 |
24 | Rating: {{ itemData.rating }}
25 |
29 |
37 |
38 |
43 | {{ itemData.description }}
44 |
45 |
46 |
47 |
${{ itemData.price }}
48 |
49 |
54 |
59 | Add to favourites
60 |
61 |
65 | Add to cart
66 |
67 |
68 |
69 |
70 |
71 |
72 |
80 |
81 |
82 |
122 |
123 |
192 |
--------------------------------------------------------------------------------
/src/views/Profile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | User profile
4 |
5 |
6 |
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
37 |
38 |
39 |
40 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: { content: ["./public/**/*.html", "./src/**/*.vue"] },
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | fontFamily: {
6 | logo: ["Princess Sofia", "serif"],
7 | sans: ["Raleway", "sans-serif"],
8 | price: ["Roboto", "sans-serif"]
9 | },
10 | extend: {
11 | borderRadius: {
12 | DEFAULT: ".25rem"
13 | },
14 | container: {
15 | center: true,
16 | padding: "1rem"
17 | },
18 | zIndex: {
19 | "-1": "-1",
20 | "-10": "-10"
21 | },
22 | translate: {
23 | hide: "-200%"
24 | },
25 | minHeight: {
26 | hero: "320px",
27 | summary: "calc(100vh - 15rem)"
28 | },
29 | colors: {
30 | primary: {
31 | light: "#EBF4FF",
32 | DEFAULT: "#3D81F5",
33 | dark: "#1450B8",
34 | darker: "#1451B8"
35 | },
36 | secondary: {
37 | light: "#FFFAF0",
38 | DEFAULT: "#F5AB3D",
39 | dark: "#C2620A"
40 | },
41 | dark: {
42 | DEFAULT: "#333333",
43 | light: "#555555",
44 | lightest: "#777777"
45 | },
46 | light: {
47 | dark: "#aaaaaa",
48 | DEFAULT: "#CCCCCC",
49 | light: "#fefefe"
50 | },
51 | background: {
52 | dark: "#4A5568",
53 | light: "#F7FAFC"
54 | }
55 | }
56 | }
57 | },
58 | variants: {
59 | extend: {}
60 | },
61 | plugins: []
62 | };
63 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "skipLibCheck": 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 | // vue.config.js
2 | module.exports = {
3 | chainWebpack: config => {
4 | config.plugin('html').tap(args => {
5 | args[0].title = 'Shuuz';
6 | return args;
7 | });
8 | },
9 | publicPath: process.env.NODE_ENV === 'production' ? '.' : '/',
10 | };
11 |
--------------------------------------------------------------------------------