├── .gitignore ├── strapi_backend ├── src │ ├── api │ │ ├── .gitkeep │ │ ├── sale │ │ │ ├── routes │ │ │ │ └── sale.js │ │ │ ├── services │ │ │ │ └── sale.js │ │ │ ├── controllers │ │ │ │ └── sale.js │ │ │ └── content-types │ │ │ │ └── sale │ │ │ │ └── schema.json │ │ ├── store │ │ │ ├── routes │ │ │ │ └── store.js │ │ │ ├── services │ │ │ │ └── store.js │ │ │ ├── controllers │ │ │ │ └── store.js │ │ │ └── content-types │ │ │ │ └── store │ │ │ │ └── schema.json │ │ ├── product │ │ │ ├── routes │ │ │ │ └── product.js │ │ │ ├── services │ │ │ │ └── product.js │ │ │ ├── controllers │ │ │ │ └── product.js │ │ │ └── content-types │ │ │ │ └── product │ │ │ │ └── schema.json │ │ └── category │ │ │ ├── routes │ │ │ └── category.js │ │ │ ├── services │ │ │ └── category.js │ │ │ ├── controllers │ │ │ └── category.js │ │ │ └── content-types │ │ │ └── category │ │ │ └── schema.json │ ├── extensions │ │ ├── .gitkeep │ │ ├── documentation │ │ │ └── config │ │ │ │ └── index.json │ │ ├── email │ │ │ └── documentation │ │ │ │ └── 1.0.0 │ │ │ │ └── email.json │ │ └── users-permissions │ │ │ └── content-types │ │ │ └── user │ │ │ └── schema.json │ ├── admin │ │ ├── webpack.config.example.js │ │ └── app.example.js │ └── index.js ├── public │ ├── uploads │ │ └── .gitkeep │ └── robots.txt ├── database │ └── migrations │ │ └── .gitkeep ├── .eslintignore ├── .tmp │ └── data.db ├── favicon.png ├── config │ ├── api.js │ ├── admin.js │ ├── server.js │ ├── middlewares.js │ └── database.js ├── .env.example ├── .editorconfig ├── .eslintrc ├── package.json ├── .gitignore └── README.md ├── frontend_remix ├── .env.example ├── .gitignore ├── app │ ├── config │ │ └── index.ts │ ├── utils │ │ ├── index.ts │ │ └── session.server.ts │ ├── routes │ │ ├── index.tsx │ │ ├── logout.tsx │ │ ├── sales.tsx │ │ ├── stores.tsx │ │ ├── products.tsx │ │ ├── categories.tsx │ │ ├── dashboard.tsx │ │ ├── products │ │ │ └── utils.ts │ │ ├── sales │ │ │ └── utils.ts │ │ ├── stores │ │ │ ├── utils.ts │ │ │ └── add.tsx │ │ ├── categories │ │ │ ├── utils.ts │ │ │ ├── add.tsx │ │ │ └── $categoryId.tsx │ │ └── login.tsx │ ├── entry.client.tsx │ ├── context │ │ ├── AuthProvider.tsx │ │ ├── TopProgressBarProvider.tsx │ │ └── ThemeContext.tsx │ ├── components │ │ ├── SideBar │ │ │ ├── DesktopSidebar.tsx │ │ │ └── MobileSidebar.tsx │ │ ├── Button.tsx │ │ ├── Layout.tsx │ │ └── Login │ │ │ └── LoginForm.tsx │ ├── root.tsx │ ├── types │ │ └── index.ts │ └── entry.server.tsx ├── jsconfig.json ├── remix.env.d.ts ├── public │ ├── favicon.ico │ └── img │ │ ├── avatar.png │ │ ├── dashboard.png │ │ ├── login-office.jpeg │ │ ├── login-office-dark.jpeg │ │ ├── create-account-office.jpeg │ │ ├── forgot-password-office.jpeg │ │ ├── create-account-office-dark.jpeg │ │ ├── forgot-password-office-dark.jpeg │ │ ├── twitter.svg │ │ └── github.svg ├── test │ ├── setup-test-env.ts │ ├── unit │ │ ├── SidebarMenuItem.test.tsx │ │ ├── MobileSidebar.test.tsx │ │ ├── DesktopSidebar.test.tsx │ │ └── loginForm.test.tsx │ ├── integration │ │ └── Dashboard.test.tsx │ └── test-utils.tsx ├── cypress │ ├── fixtures │ │ └── example.json │ ├── tsconfig.json │ ├── support │ │ ├── e2e.ts │ │ └── commands.ts │ └── e2e │ │ ├── 2-advanced-examples │ │ ├── window.cy.js │ │ ├── waiting.cy.js │ │ ├── location.cy.js │ │ ├── aliasing.cy.js │ │ ├── navigation.cy.js │ │ ├── viewport.cy.js │ │ ├── cookies.cy.js │ │ ├── files.cy.js │ │ ├── connectors.cy.js │ │ ├── misc.cy.js │ │ ├── querying.cy.js │ │ ├── traversal.cy.js │ │ ├── utilities.cy.js │ │ ├── storage.cy.js │ │ ├── cypress_api.cy.js │ │ ├── assertions.cy.js │ │ ├── spies_stubs_clocks.cy.js │ │ └── network_requests.cy.js │ │ ├── login.cy.tsx │ │ └── 1-getting-started │ │ └── todo.cy.js ├── styles │ └── app.css ├── cypress.config.ts ├── remix.config.js ├── .eslintrc.js ├── vitest.config.ts ├── tsconfig.json ├── README.md ├── package.json └── tailwind.config.js ├── .husky ├── pre-push └── pre-commit ├── .prettierrc.json ├── .vscode ├── settings.json └── launch.json ├── .eslintignore ├── .eslintrc.json ├── .prettierignore ├── .github └── workflows │ ├── testing.yml │ └── node.js.yml ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /strapi_backend/src/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strapi_backend/public/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strapi_backend/src/extensions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strapi_backend/database/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strapi_backend/src/extensions/documentation/config/index.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /strapi_backend/.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | build 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /strapi_backend/src/extensions/email/documentation/1.0.0/email.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /frontend_remix/.env.example: -------------------------------------------------------------------------------- 1 | SESSION_SECRET = "DEAR_MJ" 2 | SERVER_URL = "http://localhost:1337" -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run app:test 5 | -------------------------------------------------------------------------------- /frontend_remix/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | coverage -------------------------------------------------------------------------------- /frontend_remix/app/config/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | SERVER_URL: "http://localhost:1337", 3 | } 4 | -------------------------------------------------------------------------------- /frontend_remix/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["node_modules/cypress", "./cypress/**/*.js"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend_remix/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /strapi_backend/.tmp/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/strapi_backend/.tmp/data.db -------------------------------------------------------------------------------- /strapi_backend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/strapi_backend/favicon.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint:check 5 | npm run format:check 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "endOfLine": "crlf" 6 | } 7 | -------------------------------------------------------------------------------- /frontend_remix/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/favicon.ico -------------------------------------------------------------------------------- /frontend_remix/public/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/avatar.png -------------------------------------------------------------------------------- /frontend_remix/public/img/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/dashboard.png -------------------------------------------------------------------------------- /frontend_remix/public/img/login-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/login-office.jpeg -------------------------------------------------------------------------------- /frontend_remix/app/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const classNames = (...classes: (string | undefined | null)[]) => { 2 | return classes.filter(Boolean).join(" ") 3 | } 4 | -------------------------------------------------------------------------------- /strapi_backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /frontend_remix/public/img/login-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/login-office-dark.jpeg -------------------------------------------------------------------------------- /frontend_remix/test/setup-test-env.ts: -------------------------------------------------------------------------------- 1 | import { installGlobals } from "@remix-run/node" 2 | import "@testing-library/jest-dom/extend-expect" 3 | 4 | installGlobals() 5 | -------------------------------------------------------------------------------- /strapi_backend/config/api.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /frontend_remix/public/img/create-account-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/create-account-office.jpeg -------------------------------------------------------------------------------- /frontend_remix/public/img/forgot-password-office.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/forgot-password-office.jpeg -------------------------------------------------------------------------------- /frontend_remix/public/img/create-account-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/create-account-office-dark.jpeg -------------------------------------------------------------------------------- /frontend_remix/public/img/forgot-password-office-dark.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon0513/Inventory-Management-System/HEAD/frontend_remix/public/img/forgot-password-office-dark.jpeg -------------------------------------------------------------------------------- /strapi_backend/.env.example: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=1337 3 | APP_KEYS="toBeModified1,toBeModified2" 4 | API_TOKEN_SALT=tobemodified 5 | ADMIN_JWT_SECRET=tobemodified 6 | JWT_SECRET=tobemodified 7 | -------------------------------------------------------------------------------- /strapi_backend/config/admin.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | auth: { 3 | secret: env("ADMIN_JWT_SECRET"), 4 | }, 5 | apiToken: { 6 | salt: env("API_TOKEN_SALT"), 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /strapi_backend/config/server.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | host: env("HOST", "0.0.0.0"), 3 | port: env.int("PORT", 1337), 4 | app: { 5 | keys: env.array("APP_KEYS"), 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /frontend_remix/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#144910", 4 | "titleBar.inactiveForeground": "#000000", 5 | "titleBar.inactiveBackground": "#dfc500" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | import { redirect } from "@remix-run/node" 3 | 4 | export const loader: LoaderFunction = async () => { 5 | return redirect("/dashboard") 6 | } 7 | -------------------------------------------------------------------------------- /strapi_backend/src/api/sale/routes/sale.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * sale router 5 | */ 6 | 7 | const { createCoreRouter } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreRouter("api::sale.sale") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/sale/services/sale.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * sale service 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreService("api::sale.sale") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/store/routes/store.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * store router 5 | */ 6 | 7 | const { createCoreRouter } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreRouter("api::store.store") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/store/services/store.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * store service 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreService("api::store.store") 10 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/logout.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | 3 | import { logout } from "~/utils/session.server" 4 | 5 | export const loader: LoaderFunction = async ({ request }) => { 6 | return logout(request) 7 | } 8 | -------------------------------------------------------------------------------- /strapi_backend/src/api/product/routes/product.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * product router 5 | */ 6 | 7 | const { createCoreRouter } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreRouter("api::product.product") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/sale/controllers/sale.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * sale controller 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreController("api::sale.sale") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/category/routes/category.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * category router 5 | */ 6 | 7 | const { createCoreRouter } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreRouter("api::category.category") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/product/services/product.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * product service 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreService("api::product.product") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/store/controllers/store.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * store controller 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreController("api::store.store") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/category/services/category.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * category service 5 | */ 6 | 7 | const { createCoreService } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreService("api::category.category") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/product/controllers/product.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * product controller 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreController("api::product.product") 10 | -------------------------------------------------------------------------------- /strapi_backend/src/api/category/controllers/category.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /** 4 | * category controller 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories 8 | 9 | module.exports = createCoreController("api::category.category") 10 | -------------------------------------------------------------------------------- /frontend_remix/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": ["es5", "dom"], 6 | "types": ["cypress", "node", "@testing-library/cypress"] 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend_remix/styles/app.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | } 9 | @layer components { 10 | } 11 | @layer utilities { 12 | } 13 | -------------------------------------------------------------------------------- /frontend_remix/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress" 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: "http://localhost:3000", 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /strapi_backend/config/middlewares.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | "strapi::errors", 3 | "strapi::security", 4 | "strapi::cors", 5 | "strapi::poweredBy", 6 | "strapi::logger", 7 | "strapi::query", 8 | "strapi::body", 9 | "strapi::session", 10 | "strapi::favicon", 11 | "strapi::public", 12 | ] 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .husky 3 | .vscode 4 | frontend_remix/node_modules 5 | frontend_remix/build 6 | frontend_remix/.cache 7 | frontend_remix/coverage 8 | frontend_remix/styles 9 | frontend_remix/public/build 10 | frontend_remix/cypress/e2e/1-getting-started 11 | frontend_remix/cypress/e2e/2-advanced-examples 12 | strapi_backend/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": ["eslint:recommended", "prettier"], 8 | "overrides": [], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": {} 14 | } 15 | -------------------------------------------------------------------------------- /strapi_backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .husky 3 | .vscode 4 | .github 5 | frontend_remix/node_modules 6 | frontend_remix/build 7 | frontend_remix/.cache 8 | frontend_remix/coverage 9 | frontend_remix/styles 10 | frontend_remix/public/build 11 | frontend_remix/cypress/e2e/1-getting-started 12 | frontend_remix/cypress/e2e/2-advanced-examples 13 | strapi_backend/* -------------------------------------------------------------------------------- /strapi_backend/src/admin/webpack.config.example.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | /* eslint-disable no-unused-vars */ 4 | module.exports = (config, webpack) => { 5 | // Note: we provide webpack above so you should not `require` it 6 | // Perform customizations to webpack config 7 | // Important: return the modified config 8 | return config 9 | } 10 | -------------------------------------------------------------------------------- /frontend_remix/remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | ignoredRouteFiles: ["**/.*"], 4 | serverDependenciesToBundle: ["axios"], 5 | // appDirectory: "app", 6 | // assetsBuildDirectory: "public/build", 7 | // serverBuildPath: "build/index.js", 8 | // publicPath: "/build/", 9 | } 10 | -------------------------------------------------------------------------------- /strapi_backend/config/database.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | module.exports = ({ env }) => ({ 4 | connection: { 5 | client: "sqlite", 6 | connection: { 7 | filename: path.join( 8 | __dirname, 9 | "..", 10 | env("DATABASE_FILENAME", ".tmp/data.db") 11 | ), 12 | }, 13 | useNullAsDefault: true, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /frontend_remix/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@types/eslint').Linter.BaseConfig} */ 2 | module.exports = { 3 | extends: [ 4 | "@remix-run/eslint-config", 5 | "@remix-run/eslint-config/node", 6 | "@remix-run/eslint-config/jest-testing-library", 7 | "prettier", 8 | "plugin:cypress/recommended", 9 | ], 10 | settings: { 11 | jest: { 12 | version: 28, 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/sales.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | import { Outlet } from "@remix-run/react" 3 | import Layout from "~/components/Layout" 4 | import { requireUserId } from "~/utils/session.server" 5 | 6 | export const loader: LoaderFunction = async ({ request }) => { 7 | return await requireUserId(request) 8 | } 9 | 10 | function Sales() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | export default Sales 18 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/stores.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | import { Outlet } from "@remix-run/react" 3 | import Layout from "~/components/Layout" 4 | import { requireUserId } from "~/utils/session.server" 5 | 6 | export const loader: LoaderFunction = async ({ request }) => { 7 | return await requireUserId(request) 8 | } 9 | 10 | function Stores() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | export default Stores 18 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/products.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | import { Outlet } from "@remix-run/react" 3 | import Layout from "~/components/Layout" 4 | import { requireUserId } from "~/utils/session.server" 5 | 6 | export const loader: LoaderFunction = async ({ request }) => { 7 | return await requireUserId(request) 8 | } 9 | 10 | function Products() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | export default Products 18 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/categories.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction } from "@remix-run/node" 2 | import { Outlet } from "@remix-run/react" 3 | import Layout from "~/components/Layout" 4 | import { requireUserId } from "~/utils/session.server" 5 | 6 | export const loader: LoaderFunction = async ({ request }) => { 7 | return await requireUserId(request) 8 | } 9 | 10 | function Categories() { 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | export default Categories 18 | -------------------------------------------------------------------------------- /.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": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /strapi_backend/src/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | module.exports = { 4 | /** 5 | * An asynchronous register function that runs before 6 | * your application is initialized. 7 | * 8 | * This gives you an opportunity to extend code. 9 | */ 10 | register(/*{ strapi }*/) {}, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | bootstrap(/*{ strapi }*/) {}, 20 | } 21 | -------------------------------------------------------------------------------- /strapi_backend/src/api/sale/content-types/sale/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "sales", 4 | "info": { 5 | "singularName": "sale", 6 | "pluralName": "sales", 7 | "displayName": "Sale", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "product": { 16 | "type": "relation", 17 | "relation": "oneToOne", 18 | "target": "api::product.product" 19 | }, 20 | "quantity": { 21 | "type": "integer", 22 | "required": true 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import type { LoaderFunction, MetaFunction } from "@remix-run/node" 2 | import { Outlet } from "@remix-run/react" 3 | import Layout from "~/components/Layout" 4 | import { requireUserId } from "~/utils/session.server" 5 | 6 | export const meta: MetaFunction = () => ({ 7 | title: "Dashboard", 8 | }) 9 | 10 | export const loader: LoaderFunction = async ({ request }) => { 11 | return await requireUserId(request) 12 | } 13 | 14 | function Index() { 15 | return ( 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Index 23 | -------------------------------------------------------------------------------- /frontend_remix/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import react from "@vitejs/plugin-react" 5 | import { defineConfig } from "vite" 6 | import tsconfigPaths from "vite-tsconfig-paths" 7 | 8 | export default defineConfig({ 9 | plugins: [react(), tsconfigPaths()], 10 | test: { 11 | coverage: { 12 | provider: "c8", 13 | reporter: ["text", "json", "html"], 14 | }, 15 | globals: true, 16 | environment: "happy-dom", 17 | setupFiles: ["./test/setup-test-env.ts"], 18 | reporters: "verbose", 19 | cache: false, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /frontend_remix/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react" 2 | import { startTransition, StrictMode } from "react" 3 | import { hydrateRoot } from "react-dom/client" 4 | 5 | function hydrate() { 6 | startTransition(() => { 7 | hydrateRoot( 8 | document, 9 | 10 | 11 | 12 | ) 13 | }) 14 | } 15 | 16 | if (window.requestIdleCallback) { 17 | window.requestIdleCallback(hydrate) 18 | } else { 19 | // Safari doesn't support requestIdleCallback 20 | // https://caniuse.com/requestidlecallback 21 | window.setTimeout(hydrate, 1) 22 | } 23 | -------------------------------------------------------------------------------- /frontend_remix/public/img/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strapi_backend/src/api/category/content-types/category/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "categories", 4 | "info": { 5 | "singularName": "category", 6 | "pluralName": "categories", 7 | "displayName": "Category", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "products": { 21 | "type": "relation", 22 | "relation": "manyToMany", 23 | "target": "api::product.product", 24 | "inversedBy": "categories" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend_remix/app/context/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import type { User } from "~/types" 2 | import React from "react" 3 | 4 | export const AuthContext = React.createContext(null) 5 | AuthContext.displayName = "AuthContext" 6 | 7 | function AuthProvider({ 8 | user, 9 | ...props 10 | }: { 11 | user: User 12 | children: React.ReactNode 13 | }) { 14 | return 15 | } 16 | 17 | export const useAuthProvider = () => { 18 | const context = React.useContext(AuthContext) as User 19 | if (context === undefined) { 20 | throw new Error("useAuthProvider must be used within a AuthProvider") 21 | } 22 | return context 23 | } 24 | 25 | export default AuthProvider 26 | -------------------------------------------------------------------------------- /strapi_backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "browser": false 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": false 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "globals": { 18 | "strapi": true 19 | }, 20 | "rules": { 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "linebreak-style": ["error", "unix"], 23 | "no-console": 0, 24 | "quotes": ["error", "single"], 25 | "semi": ["error", "always"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /strapi_backend/src/api/store/content-types/store/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "stores", 4 | "info": { 5 | "singularName": "store", 6 | "pluralName": "stores", 7 | "displayName": "Store", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "address": { 21 | "type": "text", 22 | "required": true 23 | }, 24 | "products": { 25 | "type": "relation", 26 | "relation": "oneToMany", 27 | "target": "api::product.product", 28 | "mappedBy": "store" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend_remix/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands" 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /frontend_remix/public/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/window.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Window", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/window") 6 | }) 7 | 8 | it("cy.window() - get the global window object", () => { 9 | // https://on.cypress.io/window 10 | cy.window().should("have.property", "top") 11 | }) 12 | 13 | it("cy.document() - get the document object", () => { 14 | // https://on.cypress.io/document 15 | cy.document().should("have.property", "charset").and("eq", "UTF-8") 16 | }) 17 | 18 | it("cy.title() - get the title", () => { 19 | // https://on.cypress.io/title 20 | cy.title().should("include", "Kitchen Sink") 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /strapi_backend/src/admin/app.example.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | locales: [ 3 | // 'ar', 4 | // 'fr', 5 | // 'cs', 6 | // 'de', 7 | // 'dk', 8 | // 'es', 9 | // 'he', 10 | // 'id', 11 | // 'it', 12 | // 'ja', 13 | // 'ko', 14 | // 'ms', 15 | // 'nl', 16 | // 'no', 17 | // 'pl', 18 | // 'pt-BR', 19 | // 'pt', 20 | // 'ru', 21 | // 'sk', 22 | // 'sv', 23 | // 'th', 24 | // 'tr', 25 | // 'uk', 26 | // 'vi', 27 | // 'zh-Hans', 28 | // 'zh', 29 | ], 30 | } 31 | 32 | const bootstrap = (app) => { 33 | console.log(app) 34 | } 35 | 36 | export default { 37 | config, 38 | bootstrap, 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Testing 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm run setup 30 | - run: npm run app:test 31 | -------------------------------------------------------------------------------- /frontend_remix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | // "exclude": ["./cypress.config.ts"], 4 | "compilerOptions": { 5 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "module": "CommonJS", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "target": "ES2019", 13 | "strict": true, 14 | "allowJs": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "skipLibCheck": true, 21 | 22 | // Remix takes care of building everything in `remix build`. 23 | "noEmit": true, 24 | "types": [ 25 | "vitest/globals", 26 | "@testing-library/jest-dom", 27 | "cypress", 28 | "@testing-library/cypress" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /strapi_backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-backend", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A Strapi application", 6 | "scripts": { 7 | "develop": "strapi develop", 8 | "start": "strapi start", 9 | "build": "strapi build", 10 | "strapi": "strapi" 11 | }, 12 | "dependencies": { 13 | "@strapi/plugin-documentation": "^4.5.2", 14 | "@strapi/plugin-graphql": "4.5.2", 15 | "@strapi/plugin-i18n": "4.5.2", 16 | "@strapi/plugin-users-permissions": "4.5.2", 17 | "@strapi/strapi": "4.5.2", 18 | "better-sqlite3": "7.4.6" 19 | }, 20 | "author": { 21 | "name": "A Strapi developer" 22 | }, 23 | "strapi": { 24 | "uuid": "8a1db0b2-3c98-41ac-bd84-6a4bf0f9cb57" 25 | }, 26 | "engines": { 27 | "node": ">=14.19.1 <=18.x.x", 28 | "npm": ">=6.0.0" 29 | }, 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel-eslint": "^10.1.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend_remix/app/context/TopProgressBarProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | type ContextType = { 4 | progress: number 5 | setProgress: (progress: number) => void 6 | } 7 | const TopProgressBarContext = React.createContext(null) 8 | TopProgressBarContext.displayName = "TopProgressBarContext" 9 | 10 | function TopProgressBarProvider({ ...props }: { children: React.ReactNode }) { 11 | const [progress, setProgress] = React.useState(0) 12 | return ( 13 | 17 | ) 18 | } 19 | 20 | export const useTopProgressBarProvider = () => { 21 | const context = React.useContext(TopProgressBarContext) as ContextType 22 | if (context === undefined) { 23 | throw new Error( 24 | "useTopProgressBarProvider must be used within a TopProgressBarProvider" 25 | ) 26 | } 27 | return context 28 | } 29 | 30 | export default TopProgressBarProvider 31 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Deployment 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: self-hosted 14 | 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm run setup 28 | - run: | 29 | echo "${{ secrets.FRONTEND_ENV }}" > ./frontend_remix/.env 30 | - run: | 31 | echo "${{ secrets.BACKEND_ENV }}" > ./strapi_backend/.env 32 | - run: pm2 restart backend 33 | - run: pm2 restart frontend 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maruf Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend_remix/app/components/SideBar/DesktopSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react" 2 | import type { sideBarMenuType } from "./SideBarHelper" 3 | import { SideBarMenuItem } from "./SideBarHelper" 4 | 5 | function DesktopSidebar({ 6 | title, 7 | menus, 8 | }: { 9 | title: string 10 | menus: sideBarMenuType[] 11 | }) { 12 | return ( 13 | 30 | ) 31 | } 32 | 33 | export default DesktopSidebar 34 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/waiting.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Waiting", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/waiting") 6 | }) 7 | // BE CAREFUL of adding unnecessary wait times. 8 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 9 | 10 | // https://on.cypress.io/wait 11 | it("cy.wait() - wait for a specific amount of time", () => { 12 | cy.get(".wait-input1").type("Wait 1000ms after typing") 13 | cy.wait(1000) 14 | cy.get(".wait-input2").type("Wait 1000ms after typing") 15 | cy.wait(1000) 16 | cy.get(".wait-input3").type("Wait 1000ms after typing") 17 | cy.wait(1000) 18 | }) 19 | 20 | it("cy.wait() - wait for a specific route", () => { 21 | // Listen to GET to comments/1 22 | cy.intercept("GET", "**/comments/*").as("getComment") 23 | 24 | // we have code that gets a comment when 25 | // the button is clicked in scripts.js 26 | cy.get(".network-btn").click() 27 | 28 | // wait for GET comments/1 29 | cy.wait("@getComment") 30 | .its("response.statusCode") 31 | .should("be.oneOf", [200, 304]) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /frontend_remix/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | From your terminal: 8 | 9 | ```sh 10 | npm run dev 11 | ``` 12 | 13 | This starts your app in development mode, rebuilding assets on file changes. 14 | 15 | ## Deployment 16 | 17 | First, build your app for production: 18 | 19 | ```sh 20 | npm run build 21 | ``` 22 | 23 | Then run the app in production mode: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | Now you'll need to pick a host to deploy it to. 30 | 31 | ### DIY 32 | 33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready. 34 | 35 | Make sure to deploy the output of `remix build` 36 | 37 | - `build/` 38 | - `public/build/` 39 | 40 | ### Using a Template 41 | 42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. 43 | 44 | ```sh 45 | cd .. 46 | # create a new project, and pick a pre-configured host 47 | npx create-remix@latest 48 | cd my-new-remix-app 49 | # remove the new project's app (not the old one!) 50 | rm -rf app 51 | # copy your app over 52 | cp -R ../my-old-remix-app/app app 53 | ``` 54 | -------------------------------------------------------------------------------- /frontend_remix/app/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "react-router-dom" 3 | import { classNames } from "~/utils" 4 | 5 | function Button({ 6 | href, 7 | className, 8 | children, 9 | type, 10 | disabled, 11 | ...props 12 | }: { 13 | href?: string 14 | className?: string 15 | children: React.ReactNode 16 | type?: "button" | "submit" | "reset" 17 | disabled?: boolean 18 | }) { 19 | const style = classNames( 20 | "px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple", 21 | className 22 | ) 23 | 24 | return ( 25 | <> 26 | {href ? ( 27 | 28 | {children} 29 | 30 | ) : ( 31 | 39 | )} 40 | 41 | ) 42 | } 43 | 44 | export default Button 45 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/products/utils.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node" 2 | 3 | export type ActionData = { 4 | formError?: string 5 | fieldErrors?: { 6 | name: string | undefined 7 | price: string | undefined 8 | quantity: string | undefined 9 | } 10 | fields?: { 11 | name: string 12 | description: string 13 | price: string 14 | quantity: string 15 | categories: Array 16 | store: string 17 | } 18 | } 19 | 20 | export function validateName(name: unknown) { 21 | if (typeof name !== "string" || name.length < 4) { 22 | return `Product name is too short` 23 | } 24 | } 25 | 26 | export function validatePrice(price: unknown) { 27 | if (typeof price !== "string" || price.length < 1) { 28 | return "Price is required" 29 | } else { 30 | if (parseInt(price) <= 0) { 31 | return `Price can't be negative` 32 | } 33 | } 34 | } 35 | export function validateQuantity(quantity: unknown) { 36 | if (typeof quantity !== "string" || quantity.length < 1) { 37 | return "quantity is required" 38 | } else { 39 | if (parseInt(quantity) <= 0) { 40 | return `quantity can't be negative` 41 | } 42 | } 43 | } 44 | 45 | export const badRequest = (data: ActionData) => json(data, { status: 400 }) 46 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/sales/utils.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node" 2 | 3 | export type ActionData = { 4 | formError?: string 5 | fieldErrors?: { 6 | name: string | undefined 7 | price: string | undefined 8 | quantity: string | undefined 9 | } 10 | fields?: { 11 | name: string 12 | description: string 13 | price: string 14 | quantity: string 15 | categories: Array 16 | store: string 17 | } 18 | } 19 | 20 | export function validateName(name: unknown) { 21 | if (typeof name !== "string" || name.length < 4) { 22 | return `Product name is too short` 23 | } 24 | } 25 | 26 | export function validatePrice(price: unknown) { 27 | if (typeof price !== "string" || price.length < 1) { 28 | return "Price is required" 29 | } else { 30 | if (parseInt(price) <= 0) { 31 | return `Price can't be negative` 32 | } 33 | } 34 | } 35 | export function validateQuantity(quantity: unknown) { 36 | if (typeof quantity !== "string" || quantity.length < 1) { 37 | return "quantity is required" 38 | } else { 39 | if (parseInt(quantity) <= 0) { 40 | return `quantity can't be negative` 41 | } 42 | } 43 | } 44 | 45 | export const badRequest = (data: ActionData) => json(data, { status: 400 }) 46 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/stores/utils.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node" 2 | 3 | export type ActionData = { 4 | formError?: string 5 | fieldErrors?: { 6 | name: string | undefined 7 | price: string | undefined 8 | quantity: string | undefined 9 | } 10 | fields?: { 11 | name: string 12 | description: string 13 | price: string 14 | quantity: string 15 | categories: Array 16 | store: string 17 | } 18 | } 19 | 20 | export function validateName(name: unknown) { 21 | if (typeof name !== "string" || name.length < 4) { 22 | return `Product name is too short` 23 | } 24 | } 25 | 26 | export function validatePrice(price: unknown) { 27 | if (typeof price !== "string" || price.length < 1) { 28 | return "Price is required" 29 | } else { 30 | if (parseInt(price) <= 0) { 31 | return `Price can't be negative` 32 | } 33 | } 34 | } 35 | export function validateQuantity(quantity: unknown) { 36 | if (typeof quantity !== "string" || quantity.length < 1) { 37 | return "quantity is required" 38 | } else { 39 | if (parseInt(quantity) <= 0) { 40 | return `quantity can't be negative` 41 | } 42 | } 43 | } 44 | 45 | export const badRequest = (data: ActionData) => json(data, { status: 400 }) 46 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/categories/utils.ts: -------------------------------------------------------------------------------- 1 | import { json } from "@remix-run/node" 2 | 3 | export type ActionData = { 4 | formError?: string 5 | fieldErrors?: { 6 | name: string | undefined 7 | price: string | undefined 8 | quantity: string | undefined 9 | } 10 | fields?: { 11 | name: string 12 | description: string 13 | price: string 14 | quantity: string 15 | categories: Array 16 | store: string 17 | } 18 | } 19 | 20 | export function validateName(name: unknown) { 21 | if (typeof name !== "string" || name.length < 4) { 22 | return `Product name is too short` 23 | } 24 | } 25 | 26 | export function validatePrice(price: unknown) { 27 | if (typeof price !== "string" || price.length < 1) { 28 | return "Price is required" 29 | } else { 30 | if (parseInt(price) <= 0) { 31 | return `Price can't be negative` 32 | } 33 | } 34 | } 35 | export function validateQuantity(quantity: unknown) { 36 | if (typeof quantity !== "string" || quantity.length < 1) { 37 | return "quantity is required" 38 | } else { 39 | if (parseInt(quantity) <= 0) { 40 | return `quantity can't be negative` 41 | } 42 | } 43 | } 44 | 45 | export const badRequest = (data: ActionData) => json(data, { status: 400 }) 46 | -------------------------------------------------------------------------------- /strapi_backend/src/api/product/content-types/product/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "products", 4 | "info": { 5 | "singularName": "product", 6 | "pluralName": "products", 7 | "displayName": "Product", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "description": { 21 | "type": "text", 22 | "required": false 23 | }, 24 | "quantity": { 25 | "type": "integer", 26 | "required": true 27 | }, 28 | "image": { 29 | "type": "media", 30 | "multiple": false, 31 | "required": false, 32 | "allowedTypes": [ 33 | "images", 34 | "files", 35 | "videos", 36 | "audios" 37 | ] 38 | }, 39 | "categories": { 40 | "type": "relation", 41 | "relation": "manyToMany", 42 | "target": "api::category.category", 43 | "mappedBy": "products" 44 | }, 45 | "store": { 46 | "type": "relation", 47 | "relation": "manyToOne", 48 | "target": "api::store.store", 49 | "inversedBy": "products" 50 | }, 51 | "price": { 52 | "type": "integer", 53 | "required": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/location.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Location", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/location") 6 | }) 7 | 8 | it("cy.hash() - get the current URL hash", () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should("be.empty") 11 | }) 12 | 13 | it("cy.location() - get window.location", () => { 14 | // https://on.cypress.io/location 15 | cy.location().should((location) => { 16 | expect(location.hash).to.be.empty 17 | expect(location.href).to.eq( 18 | "https://example.cypress.io/commands/location" 19 | ) 20 | expect(location.host).to.eq("example.cypress.io") 21 | expect(location.hostname).to.eq("example.cypress.io") 22 | expect(location.origin).to.eq("https://example.cypress.io") 23 | expect(location.pathname).to.eq("/commands/location") 24 | expect(location.port).to.eq("") 25 | expect(location.protocol).to.eq("https:") 26 | expect(location.search).to.be.empty 27 | }) 28 | }) 29 | 30 | it("cy.url() - get the current URL", () => { 31 | // https://on.cypress.io/url 32 | cy.url().should("eq", "https://example.cypress.io/commands/location") 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/aliasing.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Aliasing", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/aliasing") 6 | }) 7 | 8 | it(".as() - alias a DOM element for later use", () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get(".as-table") 16 | .find("tbody>tr") 17 | .first() 18 | .find("td") 19 | .first() 20 | .find("button") 21 | .as("firstBtn") 22 | 23 | // when we reference the alias, we place an 24 | // @ in front of its name 25 | cy.get("@firstBtn").click() 26 | 27 | cy.get("@firstBtn") 28 | .should("have.class", "btn-success") 29 | .and("contain", "Changed") 30 | }) 31 | 32 | it(".as() - alias a route for later use", () => { 33 | // Alias the route to wait for its response 34 | cy.intercept("GET", "**/comments/*").as("getComment") 35 | 36 | // we have code that gets a comment when 37 | // the button is clicked in scripts.js 38 | cy.get(".network-btn").click() 39 | 40 | // https://on.cypress.io/wait 41 | cy.wait("@getComment").its("response.statusCode").should("eq", 200) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /frontend_remix/test/unit/SidebarMenuItem.test.tsx: -------------------------------------------------------------------------------- 1 | import type { SideBarMenuType } from "~/components/SideBar/SideBarHelper" 2 | import { MdPointOfSale } from "react-icons/md" 3 | import { SideBarMenuItem } from "~/components/SideBar/SideBarHelper" 4 | import { render, screen } from "../test-utils" 5 | 6 | const menuItem = { 7 | name: "Sales", 8 | url: "/sales/all", 9 | icon: , 10 | } 11 | 12 | // Test 1 - renders SideBarMenuItem 13 | test("renders SideBarMenuItem with title and children", () => { 14 | const menu: SideBarMenuType = { 15 | ...menuItem, 16 | children: [ 17 | { 18 | name: "Sales list", 19 | url: "/sales/all", 20 | }, 21 | { 22 | name: "Add new sale", 23 | url: "/sales/add", 24 | }, 25 | ], 26 | } 27 | render() 28 | 29 | const menuTitleElement = screen.getByLabelText("sidebar-parent-menu") 30 | expect(menuTitleElement).toHaveTextContent(menu.name) 31 | const subMenuElements = screen.queryAllByLabelText("submenu") 32 | expect(subMenuElements.length).toEqual(menu.children?.length) 33 | }) 34 | 35 | test("renders SideBarMenuItem with title only", () => { 36 | render() 37 | 38 | const menuTitleElement = screen.getByLabelText("sidebar-parent-menu") 39 | expect(menuTitleElement).toHaveTextContent(menuItem.name) 40 | }) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-management-system", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "strapi:start": "cd strapi_backend && npm run start", 8 | "strapi:dev": "cd strapi_backend && npm run develop", 9 | "app": "cd frontend_remix && npm run dev", 10 | "app:build": "cd frontend_remix && npm run build", 11 | "app:preview": "cd frontend_remix && npm run start", 12 | "app:test": "cd frontend_remix && npm run test", 13 | "app:test-watch": "cd frontend_remix && npm run test:watch", 14 | "app:coverage": "cd frontend_remix && npm run coverage", 15 | "app:e2e": "cd frontend_remix && npm run e2e", 16 | "format:check": "prettier --check .", 17 | "format:write": "prettier --write .", 18 | "lint:check": "eslint .", 19 | "lint:fix": "eslint --fix .", 20 | "prepare": "husky install", 21 | "setup": "npm i && cd strapi_backend && npm i && cd .. && cd frontend_remix && npm i" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/maruffahmed/Inventory-Management-System.git" 26 | }, 27 | "author": "Md Maruf Ahmed", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/maruffahmed/Inventory-Management-System/issues" 31 | }, 32 | "homepage": "https://github.com/maruffahmed/Inventory-Management-System#readme", 33 | "devDependencies": { 34 | "eslint": "^8.25.0", 35 | "eslint-config-prettier": "^8.5.0", 36 | "husky": "^8.0.1", 37 | "prettier": "^2.7.1" 38 | }, 39 | "dependencies": { 40 | "moment": "^2.29.4", 41 | "react-to-print": "^2.14.10" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /strapi_backend/.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | *.log 69 | *.sql 70 | *.sqlite 71 | *.sqlite3 72 | 73 | 74 | ############################ 75 | # Misc. 76 | ############################ 77 | 78 | *# 79 | ssl 80 | .idea 81 | nbproject 82 | public/uploads/* 83 | !public/uploads/.gitkeep 84 | 85 | ############################ 86 | # Node.js 87 | ############################ 88 | 89 | lib-cov 90 | lcov.info 91 | pids 92 | logs 93 | results 94 | node_modules 95 | .node_history 96 | 97 | ############################ 98 | # Tests 99 | ############################ 100 | 101 | testApp 102 | coverage 103 | 104 | ############################ 105 | # Strapi 106 | ############################ 107 | 108 | .env 109 | license.txt 110 | exports 111 | *.cache 112 | dist 113 | build 114 | .strapi-updater.json 115 | -------------------------------------------------------------------------------- /frontend_remix/app/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useThemeProvider } from "~/context/ThemeContext" 3 | import { useTopProgressBarProvider } from "~/context/TopProgressBarProvider" 4 | import { classNames } from "~/utils" 5 | import Header from "./Header" 6 | import DesktopSidebar from "./SideBar/DesktopSidebar" 7 | import MobileSidebar from "./SideBar/MobileSidebar" 8 | import LoadingBar from "react-top-loading-bar" 9 | import { sideBarMenus } from "./SideBar/SideBarHelper" 10 | 11 | function Layout({ children }: { children: React.ReactNode }) { 12 | const { isDarkMode, isSideMenuOpen, toggleDarkMode } = useThemeProvider() 13 | // top progress bar 14 | const { progress, setProgress } = useTopProgressBarProvider() 15 | React.useEffect(() => { 16 | setProgress(100) 17 | }, [setProgress]) 18 | return ( 19 | <> 20 | 21 |
27 | 31 | 32 |
33 |
38 | {children} 39 |
40 |
41 | 42 | ) 43 | } 44 | 45 | export default Layout 46 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/login.cy.tsx: -------------------------------------------------------------------------------- 1 | import { sideBarMenusTotalSize } from "../../app/components/SideBar/SideBarHelper" 2 | 3 | describe("Admin workflow", () => { 4 | beforeEach(() => { 5 | cy.visit("/") 6 | }) 7 | 8 | it("Check dashboard data", () => { 9 | cy.get("html").should("have.class", "dark") 10 | 11 | cy.findByRole("textbox", { name: /email/i }).type( 12 | "testadmin@gmail.com", 13 | { force: true } 14 | ) 15 | 16 | cy.findByLabelText(/password/i).type("test123") 17 | cy.findByRole("button", { name: /log in/i }) 18 | .should("not.be.disabled") 19 | .click() 20 | cy.url().should("include", "/dashboard") 21 | 22 | cy.findByRole("main").within(() => { 23 | cy.findByRole("heading", { name: /home dashboard/i }).should( 24 | "exist" 25 | ) 26 | cy.findByText(/Sales Revenue/i).should("exist") 27 | cy.findByText(/Products Value/i).should("exist") 28 | cy.findByText(/Total Products/i).should("exist") 29 | cy.findByText(/Total Categories/i).should("exist") 30 | }) 31 | 32 | cy.findByRole("complementary").within(async () => { 33 | cy.findAllByRole("listitem").should( 34 | "have.length", 35 | sideBarMenusTotalSize 36 | ) 37 | }) 38 | 39 | cy.findByRole("banner").within(() => { 40 | cy.findByRole("img", { hidden: true }).should("exist").click() 41 | cy.findByRole("menu").within(() => { 42 | cy.findByRole("menuitem", { 43 | name: /Admin/i, 44 | }).should("exist") 45 | cy.findByRole("menuitem", { name: /Log out/i }) 46 | .should("exist") 47 | .click() 48 | }) 49 | }) 50 | cy.url().should("include", "/login") 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /strapi_backend/src/extensions/users-permissions/content-types/user/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "up_users", 4 | "info": { 5 | "name": "user", 6 | "description": "", 7 | "singularName": "user", 8 | "pluralName": "users", 9 | "displayName": "User" 10 | }, 11 | "options": { 12 | "draftAndPublish": false, 13 | "timestamps": true 14 | }, 15 | "attributes": { 16 | "username": { 17 | "type": "string", 18 | "minLength": 3, 19 | "unique": true, 20 | "configurable": false, 21 | "required": true 22 | }, 23 | "email": { 24 | "type": "email", 25 | "minLength": 6, 26 | "configurable": false, 27 | "required": true 28 | }, 29 | "provider": { 30 | "type": "string", 31 | "configurable": false 32 | }, 33 | "password": { 34 | "type": "password", 35 | "minLength": 6, 36 | "configurable": false, 37 | "private": true 38 | }, 39 | "resetPasswordToken": { 40 | "type": "string", 41 | "configurable": false, 42 | "private": true 43 | }, 44 | "confirmationToken": { 45 | "type": "string", 46 | "configurable": false, 47 | "private": true 48 | }, 49 | "confirmed": { 50 | "type": "boolean", 51 | "default": false, 52 | "configurable": false 53 | }, 54 | "blocked": { 55 | "type": "boolean", 56 | "default": false, 57 | "configurable": false 58 | }, 59 | "role": { 60 | "type": "relation", 61 | "relation": "manyToOne", 62 | "target": "plugin::users-permissions.role", 63 | "inversedBy": "users", 64 | "configurable": false 65 | }, 66 | "avatar": { 67 | "allowedTypes": [ 68 | "images", 69 | "files", 70 | "videos", 71 | "audios" 72 | ], 73 | "type": "media", 74 | "multiple": false, 75 | "required": false 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend_remix/test/unit/MobileSidebar.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "test/test-utils" 2 | import userEvent from "@testing-library/user-event" 3 | import MobileSidebar from "~/components/SideBar/MobileSidebar" 4 | import { sideBarMenus } from "~/components/SideBar/SideBarHelper" 5 | import * as ThemeProviders from "~/context/ThemeContext" 6 | 7 | const user = userEvent.setup() 8 | const setIsSideMenuOpen = vi.fn() 9 | beforeEach(() => { 10 | vi.spyOn(ThemeProviders, "useThemeProvider").mockImplementation(() => ({ 11 | isSideMenuOpen: true, 12 | setIsSideMenuOpen, 13 | isDarkMode: true, 14 | setIsDarkMode: vi.fn(), 15 | toggleDarkMode: vi.fn(), 16 | })) 17 | }) 18 | afterEach(() => { 19 | vi.clearAllMocks() 20 | vi.restoreAllMocks() 21 | }) 22 | const title = "IMS - Fantastic 5" 23 | // Test 1 - renders MobileSidebar 24 | test("renders MobileSidebar with title", () => { 25 | render() 26 | // screen.debug() 27 | const titleElement = screen.getByRole("link", { 28 | name: title, 29 | }) 30 | expect(titleElement).toHaveTextContent(title) 31 | }) 32 | 33 | // Test 2 - renders all sidebar links 34 | test("renders all MobileSidebar menus", () => { 35 | render() 36 | // screen.debug() 37 | const parentMenuElement = screen.queryAllByLabelText("sidebar-parent-menu") 38 | expect(parentMenuElement.length).toEqual(sideBarMenus.length) 39 | }) 40 | 41 | // Test 3 - click overlay to close the sidebar 42 | test("Click overlay to close the MobileSidebar", async () => { 43 | render() 44 | const overlayElement = screen.getByLabelText("mobile-sidebar-overlay") 45 | 46 | expect(overlayElement).toBeInTheDocument() 47 | await user.click(overlayElement) 48 | expect(setIsSideMenuOpen).toHaveBeenCalled() 49 | expect(setIsSideMenuOpen).toHaveBeenCalledTimes(1) 50 | }) 51 | -------------------------------------------------------------------------------- /frontend_remix/test/integration/Dashboard.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "../test-utils" 2 | import type { LoaderData } from "~/routes/dashboard/index" 3 | import Dashboard, { cardsData } from "~/routes/dashboard/index" 4 | 5 | const data: LoaderData = { 6 | "Sales Revenue": "৳ 2,000", 7 | "Products Value": "৳ 1,000", 8 | "Total Products": "7", 9 | "Total Categories": "6", 10 | } 11 | 12 | // Mocking remix-run/react module 13 | vi.mock("@remix-run/react", async () => { 14 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 15 | const actual = await vi.importActual( 16 | "@remix-run/react" 17 | ) 18 | const useLoaderData = vi.fn().mockImplementation(() => data) 19 | return { ...actual, useLoaderData } 20 | }) 21 | 22 | test("Dashboard is rendering with data", async () => { 23 | const labelTexts = cardsData.reduce( 24 | (acumulator, value) => ({ ...acumulator, [value.title]: value.title }), 25 | {} 26 | ) as Record 27 | render() 28 | // screen.debug() 29 | 30 | const salesRevenueElement = screen.getByRole("generic", { 31 | name: labelTexts["Sales Revenue"], 32 | }) 33 | const productValueElement = screen.getByRole("generic", { 34 | name: labelTexts["Products Value"], 35 | }) 36 | const totalProductElement = screen.getByRole("generic", { 37 | name: labelTexts["Total Products"], 38 | }) 39 | const totalCategoriesElement = screen.getByRole("generic", { 40 | name: labelTexts["Total Categories"], 41 | }) 42 | // Check all cards are rendered with currect data 43 | expect(salesRevenueElement).toHaveTextContent(data["Sales Revenue"]) 44 | expect(productValueElement).toHaveTextContent(data["Products Value"]) 45 | expect(totalProductElement).toHaveTextContent(data["Total Products"]) 46 | expect(totalCategoriesElement).toHaveTextContent(data["Total Categories"]) 47 | }) 48 | -------------------------------------------------------------------------------- /frontend_remix/cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import "@testing-library/cypress/add-commands" 3 | 4 | Cypress.on("uncaught:exception", (err) => { 5 | // we check if the error is 6 | console.log("uncaught:exception", err.message) 7 | if ( 8 | err.message.includes("Minified React error #418;") || 9 | err.message.includes("Minified React error #423;") || 10 | err.message.includes("Hydration failed") || 11 | err.message.includes("There was an error while hydrating") 12 | ) { 13 | return false 14 | } 15 | }) 16 | 17 | // *********************************************** 18 | // This example commands.ts shows you how to 19 | // create various custom commands and overwrite 20 | // existing commands. 21 | // 22 | // For more comprehensive examples of custom 23 | // commands please read more here: 24 | // https://on.cypress.io/custom-commands 25 | // *********************************************** 26 | // 27 | // 28 | // -- This is a parent command -- 29 | // Cypress.Commands.add('login', (email, password) => { ... }) 30 | // 31 | // 32 | // -- This is a child command -- 33 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 34 | // 35 | // 36 | // -- This is a dual command -- 37 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 38 | // 39 | // 40 | // -- This will overwrite an existing command -- 41 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 42 | // 43 | // declare global { 44 | // namespace Cypress { 45 | // interface Chainable { 46 | // login(email: string, password: string): Chainable 47 | // drag(subject: string, options?: Partial): Chainable 48 | // dismiss(subject: string, options?: Partial): Chainable 49 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 50 | // } 51 | // } 52 | // } 53 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/navigation.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Navigation", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io") 6 | cy.get(".navbar-nav").contains("Commands").click() 7 | cy.get(".dropdown-menu").contains("Navigation").click() 8 | }) 9 | 10 | it("cy.go() - go back or forward in the browser's history", () => { 11 | // https://on.cypress.io/go 12 | 13 | cy.location("pathname").should("include", "navigation") 14 | 15 | cy.go("back") 16 | cy.location("pathname").should("not.include", "navigation") 17 | 18 | cy.go("forward") 19 | cy.location("pathname").should("include", "navigation") 20 | 21 | // clicking back 22 | cy.go(-1) 23 | cy.location("pathname").should("not.include", "navigation") 24 | 25 | // clicking forward 26 | cy.go(1) 27 | cy.location("pathname").should("include", "navigation") 28 | }) 29 | 30 | it("cy.reload() - reload the page", () => { 31 | // https://on.cypress.io/reload 32 | cy.reload() 33 | 34 | // reload the page without using the cache 35 | cy.reload(true) 36 | }) 37 | 38 | it("cy.visit() - visit a remote url", () => { 39 | // https://on.cypress.io/visit 40 | 41 | // Visit any sub-domain of your current domain 42 | 43 | // Pass options to the visit 44 | cy.visit("https://example.cypress.io/commands/navigation", { 45 | timeout: 50000, // increase total time for the visit to resolve 46 | onBeforeLoad(contentWindow) { 47 | // contentWindow is the remote page's window object 48 | expect(typeof contentWindow === "object").to.be.true 49 | }, 50 | onLoad(contentWindow) { 51 | // contentWindow is the remote page's window object 52 | expect(typeof contentWindow === "object").to.be.true 53 | }, 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /frontend_remix/app/components/SideBar/MobileSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@remix-run/react" 2 | import { useThemeProvider } from "~/context/ThemeContext" 3 | import type { sideBarMenuType } from "./SideBarHelper" 4 | import { SideBarMenuItem } from "./SideBarHelper" 5 | 6 | function MobileSidebar({ 7 | title, 8 | menus, 9 | }: { 10 | title: string 11 | menus: sideBarMenuType[] 12 | }) { 13 | const { setIsSideMenuOpen, isSideMenuOpen } = useThemeProvider() 14 | return ( 15 | <> 16 | {isSideMenuOpen && ( 17 | <> 18 |
setIsSideMenuOpen(false)} 20 | aria-label="mobile-sidebar-overlay" 21 | className="fixed inset-0 z-10 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center" 22 | >
23 | 24 | 44 | 45 | )} 46 | 47 | ) 48 | } 49 | 50 | export default MobileSidebar 51 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/viewport.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Viewport", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/viewport") 6 | }) 7 | 8 | it("cy.viewport() - set the viewport size and dimension", () => { 9 | // https://on.cypress.io/viewport 10 | 11 | cy.get("#navbar").should("be.visible") 12 | cy.viewport(320, 480) 13 | 14 | // the navbar should have collapse since our screen is smaller 15 | cy.get("#navbar").should("not.be.visible") 16 | cy.get(".navbar-toggle").should("be.visible").click() 17 | cy.get(".nav").find("a").should("be.visible") 18 | 19 | // lets see what our app looks like on a super large screen 20 | cy.viewport(2999, 2999) 21 | 22 | // cy.viewport() accepts a set of preset sizes 23 | // to easily set the screen to a device's width and height 24 | 25 | // We added a cy.wait() between each viewport change so you can see 26 | // the change otherwise it is a little too fast to see :) 27 | 28 | cy.viewport("macbook-15") 29 | cy.wait(200) 30 | cy.viewport("macbook-13") 31 | cy.wait(200) 32 | cy.viewport("macbook-11") 33 | cy.wait(200) 34 | cy.viewport("ipad-2") 35 | cy.wait(200) 36 | cy.viewport("ipad-mini") 37 | cy.wait(200) 38 | cy.viewport("iphone-6+") 39 | cy.wait(200) 40 | cy.viewport("iphone-6") 41 | cy.wait(200) 42 | cy.viewport("iphone-5") 43 | cy.wait(200) 44 | cy.viewport("iphone-4") 45 | cy.wait(200) 46 | cy.viewport("iphone-3") 47 | cy.wait(200) 48 | 49 | // cy.viewport() accepts an orientation for all presets 50 | // the default orientation is 'portrait' 51 | cy.viewport("ipad-2", "portrait") 52 | cy.wait(200) 53 | cy.viewport("iphone-4", "landscape") 54 | cy.wait(200) 55 | 56 | // The viewport will be reset back to the default dimensions 57 | // in between tests (the default can be set in cypress.config.{js|ts}) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /frontend_remix/app/context/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export interface ContextType { 4 | isSideMenuOpen: Boolean 5 | setIsSideMenuOpen: React.Dispatch> 6 | isDarkMode: Boolean 7 | setIsDarkMode: Function 8 | toggleDarkMode: Function 9 | } 10 | 11 | const ThemeContext = React.createContext(null) 12 | ThemeContext.displayName = "ThemeContext" 13 | 14 | const setTheme = (theme: string, setState: Function, stateValue: Boolean) => { 15 | localStorage.setItem("theme", theme) 16 | document.documentElement.className = theme 17 | setState(stateValue) 18 | } 19 | 20 | function ThemeProvider(props: any) { 21 | const [isSideMenuOpen, setIsSideMenuOpen] = React.useState(false) 22 | const [isDarkMode, setIsDarkMode] = React.useState(false) 23 | 24 | React.useEffect(() => { 25 | // On page load or when changing themes, best to add inline in `head` to avoid FOUC 26 | if ( 27 | window.localStorage.theme === "dark" || 28 | (!("theme" in localStorage) && 29 | window.matchMedia("(prefers-color-scheme: dark)").matches) 30 | ) { 31 | setTheme("dark", setIsDarkMode, true) 32 | } else { 33 | setTheme("light", setIsDarkMode, false) 34 | } 35 | // Whenever the user explicitly chooses to respect the OS preference 36 | // window.localStorage.removeItem("theme") 37 | }, []) 38 | const toggleDarkMode = () => { 39 | if (window.localStorage.theme === "dark") { 40 | setTheme("light", setIsDarkMode, false) 41 | } else { 42 | setTheme("dark", setIsDarkMode, true) 43 | } 44 | } 45 | return ( 46 | 56 | ) 57 | } 58 | 59 | export const useThemeProvider = () => { 60 | const context = React.useContext(ThemeContext) as ContextType 61 | if (context === undefined) { 62 | throw new Error("useTheme must be used within a ThemeProvider") 63 | } 64 | return context 65 | } 66 | 67 | export default ThemeProvider 68 | -------------------------------------------------------------------------------- /frontend_remix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "sideEffects": false, 4 | "scripts": { 5 | "start": "remix-serve build", 6 | "build": "npm run build:css && remix build", 7 | "build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css", 8 | "dev": "SET NODE_ENV=development & concurrently \"npm run dev:css\" \"remix dev\"", 9 | "dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css", 10 | "test": "vitest --watch=false", 11 | "test:watch": "vitest --watch", 12 | "coverage": "vitest run --coverage", 13 | "e2e": "cypress open" 14 | }, 15 | "dependencies": { 16 | "@headlessui/react": "^1.7.4", 17 | "@heroicons/react": "^2.0.13", 18 | "@remix-run/node": "^1.7.6", 19 | "@remix-run/react": "^1.7.6", 20 | "@remix-run/serve": "^1.7.6", 21 | "axios": "^1.2.0", 22 | "chart.js": "^3.9.1", 23 | "isbot": "^3.6.5", 24 | "numeral": "^2.0.6", 25 | "react": "^18.2.0", 26 | "react-chartjs-2": "^4.3.1", 27 | "react-dom": "^18.2.0", 28 | "react-icons": "^4.7.1", 29 | "react-top-loading-bar": "^2.3.1", 30 | "validator": "^13.7.0" 31 | }, 32 | "devDependencies": { 33 | "@remix-run/dev": "^1.7.6", 34 | "@remix-run/eslint-config": "^1.7.6", 35 | "@tailwindcss/forms": "^0.5.3", 36 | "@testing-library/cypress": "^9.0.0", 37 | "@testing-library/jest-dom": "^5.16.5", 38 | "@testing-library/react": "^13.4.0", 39 | "@testing-library/user-event": "^14.4.3", 40 | "@types/eslint": "^8.4.10", 41 | "@types/numeral": "^2.0.2", 42 | "@types/react": "^18.0.25", 43 | "@types/react-dom": "^18.0.8", 44 | "@types/validator": "^13.7.10", 45 | "@vitejs/plugin-react": "^3.0.0", 46 | "@vitest/coverage-c8": "^0.26.0", 47 | "autoprefixer": "^10.4.13", 48 | "color": "^4.2.3", 49 | "concurrently": "^7.6.0", 50 | "cypress": "^12.3.0", 51 | "eslint": "^8.27.0", 52 | "eslint-plugin-cypress": "^2.12.1", 53 | "happy-dom": "^8.1.0", 54 | "postcss": "^8.4.19", 55 | "tailwindcss": "^3.2.4", 56 | "typescript": "^4.9.4", 57 | "vite-tsconfig-paths": "^4.0.3", 58 | "vitest": "^0.26.1" 59 | }, 60 | "engines": { 61 | "node": ">=14" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inventory Management System 2 | 3 | This is a software engineering course project. By the time of development of this project, I lead a team of 5 members to follow the Agile development methodology. It includes requirement engineering, use case diagram, activity diagram, risk analysis, RMM plans, DFD, and ERD. 4 | 5 | 6 | 7 | ## Getting Started 8 | 9 | This instruction will get you a copy of this project up and running on your local machine 10 | 11 | ### Prerequisites 12 | 13 | You need [Node JS](https://nodejs.org) installed on your local machine. 14 | 15 | ### Installing ⚙️ 16 | 17 | Run the followning command to install all the packages: 18 | 19 | ``` 20 | npm run setup 21 | ``` 22 | 23 | #### Setup environment variable 24 | 25 | Set the following environment variable to `strapi_backend` directory. Also, an example file is given with the name of `.env.example`: 26 | 27 | ``` 28 | HOST=0.0.0.0 29 | PORT=1337 30 | APP_KEYS="toBeModified1,toBeModified2" 31 | API_TOKEN_SALT=tobemodified 32 | ADMIN_JWT_SECRET=tobemodified 33 | JWT_SECRET=tobemodified 34 | ``` 35 | 36 | You can set the avobe environment variable as it is. 37 | 38 | Set the following environment variable to `frontend_remix` directory. Also, an example file is given with the name of `.env.example`: 39 | 40 | ``` 41 | SESSION_SECRET = "ANYTHING_YOU_WANT" 42 | SERVER_URL = "STRAPI_SERVER_URL_LIKE_http://localhost:1337" 43 | ``` 44 | 45 | #### Run 🏃🏻‍♂️ 46 | 47 | To run the strapi backend server: 48 | 49 | ``` 50 | npm run strapi:dev 51 | ``` 52 | 53 | An server will be run at http://localhost:1337 54 | 55 | To run the frontend server: 56 | 57 | ``` 58 | npm run app 59 | ``` 60 | 61 | Frontend server will be run at http://localhost:3000 62 | 63 | ## Built With 🏗️👷🏻 64 | 65 | - [Strapi](https://strapi.io/) - Strapi is the leading open-source headless CMS. 66 | - [Remix](https://remix.run/) - Remix is a full stack web framework 67 | - [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework packed with classes 68 | - [NodeJs](https://nodejs.org/en/) - Node.js® is an open-source, cross-platform JavaScript runtime environment. 69 | 70 | ## Credit 71 | 72 | - [Windmill Dashboard](https://github.com/estevanmaito/windmill-dashboard) - A multi theme, completely accessible, with components and pages examples, ready for production dashboard. 73 | 74 | ## Authors 75 | 76 | - **Md Maruf Ahmed** - _Software Engineer_ 77 | -------------------------------------------------------------------------------- /strapi_backend/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Getting started with Strapi 2 | 3 | Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html) (CLI) which lets you scaffold and manage your project in seconds. 4 | 5 | ### `develop` 6 | 7 | Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-develop) 8 | 9 | ``` 10 | npm run develop 11 | # or 12 | yarn develop 13 | ``` 14 | 15 | ### `start` 16 | 17 | Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-start) 18 | 19 | ``` 20 | npm run start 21 | # or 22 | yarn start 23 | ``` 24 | 25 | ### `build` 26 | 27 | Build your admin panel. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-build) 28 | 29 | ``` 30 | npm run build 31 | # or 32 | yarn build 33 | ``` 34 | 35 | ## ⚙️ Deployment 36 | 37 | Strapi gives you many possible deployment options for your project. Find the one that suits you on the [deployment section of the documentation](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html). 38 | 39 | ## 📚 Learn more 40 | 41 | - [Resource center](https://strapi.io/resource-center) - Strapi resource center. 42 | - [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation. 43 | - [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community. 44 | - [Strapi blog](https://docs.strapi.io) - Official Strapi blog containing articles made by the Strapi team and the community. 45 | - [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements. 46 | 47 | Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome! 48 | 49 | ## ✨ Community 50 | 51 | - [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team. 52 | - [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members. 53 | - [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi. 54 | 55 | --- 56 | 57 | 🤫 Psst! [Strapi is hiring](https://strapi.io/careers). 58 | -------------------------------------------------------------------------------- /frontend_remix/test/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import type { RenderOptions } from "@testing-library/react" 2 | import type { ReactElement } from "react" 3 | import type { User } from "~/types" 4 | import { render } from "@testing-library/react" 5 | import AuthProvider from "~/context/AuthProvider" 6 | import { MemoryRouter } from "react-router-dom" 7 | import ThemeProvider from "~/context/ThemeContext" 8 | 9 | export const dummyUser: User = { 10 | id: 3, 11 | username: "Md Maruf Ahmed", 12 | email: "maruf@gmail.com", 13 | confirmed: true, 14 | blocked: false, 15 | avatar: { 16 | id: 4, 17 | name: "IMG_20220801_035813 (Large).jpg", 18 | width: 1080, 19 | height: 1282, 20 | formats: { 21 | thumbnail: { 22 | name: "thumbnail_IMG_20220801_035813 (Large).jpg", 23 | hash: "thumbnail_IMG_20220801_035813_Large_644e000333", 24 | ext: ".jpg", 25 | mime: "image/jpeg", 26 | path: null, 27 | width: 132, 28 | height: 156, 29 | size: 6.53, 30 | url: "/uploads/thumbnail_IMG_20220801_035813_Large_644e000333.jpg", 31 | }, 32 | small: { 33 | name: "small_IMG_20220801_035813 (Large).jpg", 34 | hash: "small_IMG_20220801_035813_Large_644e000333", 35 | ext: ".jpg", 36 | mime: "image/jpeg", 37 | path: null, 38 | width: 421, 39 | height: 500, 40 | size: 45.99, 41 | url: "/uploads/small_IMG_20220801_035813_Large_644e000333.jpg", 42 | }, 43 | }, 44 | hash: "IMG_20220801_035813_Large_644e000333", 45 | ext: ".jpg", 46 | mime: "image/jpeg", 47 | size: 227.74, 48 | url: "/uploads/IMG_20220801_035813_Large_644e000333.jpg", 49 | previewUrl: null, 50 | }, 51 | role: { 52 | id: 3, 53 | name: "Admin", 54 | description: "This role is for the admin who can access everything", 55 | type: "admin", 56 | }, 57 | } 58 | 59 | const AllTheProviders = ({ children }: { children: React.ReactNode }) => { 60 | return ( 61 | 62 | 63 | {children} 64 | 65 | 66 | ) 67 | } 68 | 69 | const customRender = ( 70 | ui: ReactElement, 71 | options?: Omit 72 | ) => render(ui, { wrapper: AllTheProviders, ...options }) 73 | 74 | // re-export everything 75 | export * from "@testing-library/react" 76 | 77 | // override render method 78 | export { customRender as render } 79 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/cookies.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Cookies", () => { 4 | beforeEach(() => { 5 | Cypress.Cookies.debug(true) 6 | 7 | cy.visit("https://example.cypress.io/commands/cookies") 8 | 9 | // clear cookies again after visiting to remove 10 | // any 3rd party cookies picked up such as cloudflare 11 | cy.clearCookies() 12 | }) 13 | 14 | it("cy.getCookie() - get a browser cookie", () => { 15 | // https://on.cypress.io/getcookie 16 | cy.get("#getCookie .set-a-cookie").click() 17 | 18 | // cy.getCookie() yields a cookie object 19 | cy.getCookie("token").should("have.property", "value", "123ABC") 20 | }) 21 | 22 | it("cy.getCookies() - get browser cookies", () => { 23 | // https://on.cypress.io/getcookies 24 | cy.getCookies().should("be.empty") 25 | 26 | cy.get("#getCookies .set-a-cookie").click() 27 | 28 | // cy.getCookies() yields an array of cookies 29 | cy.getCookies() 30 | .should("have.length", 1) 31 | .should((cookies) => { 32 | // each cookie has these properties 33 | expect(cookies[0]).to.have.property("name", "token") 34 | expect(cookies[0]).to.have.property("value", "123ABC") 35 | expect(cookies[0]).to.have.property("httpOnly", false) 36 | expect(cookies[0]).to.have.property("secure", false) 37 | expect(cookies[0]).to.have.property("domain") 38 | expect(cookies[0]).to.have.property("path") 39 | }) 40 | }) 41 | 42 | it("cy.setCookie() - set a browser cookie", () => { 43 | // https://on.cypress.io/setcookie 44 | cy.getCookies().should("be.empty") 45 | 46 | cy.setCookie("foo", "bar") 47 | 48 | // cy.getCookie() yields a cookie object 49 | cy.getCookie("foo").should("have.property", "value", "bar") 50 | }) 51 | 52 | it("cy.clearCookie() - clear a browser cookie", () => { 53 | // https://on.cypress.io/clearcookie 54 | cy.getCookie("token").should("be.null") 55 | 56 | cy.get("#clearCookie .set-a-cookie").click() 57 | 58 | cy.getCookie("token").should("have.property", "value", "123ABC") 59 | 60 | // cy.clearCookies() yields null 61 | cy.clearCookie("token").should("be.null") 62 | 63 | cy.getCookie("token").should("be.null") 64 | }) 65 | 66 | it("cy.clearCookies() - clear browser cookies", () => { 67 | // https://on.cypress.io/clearcookies 68 | cy.getCookies().should("be.empty") 69 | 70 | cy.get("#clearCookies .set-a-cookie").click() 71 | 72 | cy.getCookies().should("have.length", 1) 73 | 74 | // cy.clearCookies() yields null 75 | cy.clearCookies() 76 | 77 | cy.getCookies().should("be.empty") 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /frontend_remix/test/unit/DesktopSidebar.test.tsx: -------------------------------------------------------------------------------- 1 | import type { User } from "~/types" 2 | import { dummyUser, render, screen } from "../test-utils" 3 | import DesktopSidebar from "~/components/SideBar/DesktopSidebar" 4 | import { sideBarMenus } from "~/components/SideBar/SideBarHelper" 5 | import * as AuthProviders from "~/context/AuthProvider" 6 | 7 | afterEach(() => { 8 | vi.clearAllMocks() 9 | vi.restoreAllMocks() 10 | }) 11 | const title = "IMS - Fantastic 5" 12 | // Test 1 - renders DesktopSidebar 13 | test("renders DesktopSidebar with title", () => { 14 | render() 15 | // screen.debug() 16 | const titleElement = screen.getByRole("link", { 17 | name: title, 18 | }) 19 | expect(titleElement).toHaveTextContent(title) 20 | }) 21 | // Test 2 - renders all sidebar links 22 | test("renders all DesktopSidebar menus", () => { 23 | render() 24 | // screen.debug() 25 | const parentMenuElement = screen.queryAllByLabelText("sidebar-parent-menu") 26 | expect(parentMenuElement.length).toEqual(sideBarMenus.length) 27 | }) 28 | // Test 3 - renders DesktopSidebar with empty menu 29 | test("render DesktopSidebar with empty menu", () => { 30 | render() 31 | // screen.debug() 32 | const parentMenuElement = screen.queryAllByLabelText("sidebar-parent-menu") 33 | expect(parentMenuElement.length).toEqual(0) 34 | }) 35 | // Test 4 - renders DesktopSidebar with seller access 36 | test("render DesktopSidebar with Seller access", () => { 37 | const userAccessOptions = ["Dashboard", "Sales", "Sales Report"] 38 | const mockUser: User = { 39 | ...dummyUser, 40 | role: { ...dummyUser.role, name: "Seller" }, 41 | } 42 | vi.spyOn(AuthProviders, "useAuthProvider").mockImplementation( 43 | () => mockUser 44 | ) 45 | 46 | render() 47 | 48 | const parentMenuElement = screen.queryAllByLabelText("sidebar-parent-menu") 49 | expect(parentMenuElement.length).toEqual(userAccessOptions.length) 50 | userAccessOptions.forEach((optionName, index) => { 51 | expect(parentMenuElement[index]).toHaveTextContent(optionName) 52 | }) 53 | }) 54 | // Test 4 - renders DesktopSidebar with moderator access 55 | test("render DesktopSidebar with Moderator access", () => { 56 | const userAccessOptions = ["Dashboard", "Products", "Categories"] 57 | const mockUser: User = { 58 | ...dummyUser, 59 | role: { ...dummyUser.role, name: "Moderator" }, 60 | } 61 | vi.spyOn(AuthProviders, "useAuthProvider").mockImplementation( 62 | () => mockUser 63 | ) 64 | 65 | render() 66 | 67 | const parentMenuElement = screen.queryAllByLabelText("sidebar-parent-menu") 68 | expect(parentMenuElement.length).toEqual(userAccessOptions.length) 69 | userAccessOptions.forEach((optionName, index) => { 70 | expect(parentMenuElement[index]).toHaveTextContent(optionName) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /frontend_remix/app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { User } from "./types" 2 | import type { 3 | LinksFunction, 4 | LoaderFunction, 5 | MetaFunction, 6 | } from "@remix-run/node" 7 | import { json } from "@remix-run/node" 8 | import { 9 | Links, 10 | LiveReload, 11 | Meta, 12 | Outlet, 13 | Scripts, 14 | ScrollRestoration, 15 | useCatch, 16 | useLoaderData, 17 | } from "@remix-run/react" 18 | import React from "react" 19 | import ThemeProvider from "./context/ThemeContext" 20 | import styles from "./styles/app.css" 21 | import { getUser } from "./utils/session.server" 22 | import AuthProvider from "./context/AuthProvider" 23 | import TopProgressBarProvider from "./context/TopProgressBarProvider" 24 | 25 | export const links: LinksFunction = () => { 26 | return [{ rel: "stylesheet", href: styles }] 27 | } 28 | 29 | export const meta: MetaFunction = () => ({ 30 | charset: "utf-8", 31 | viewport: "width=device-width,initial-scale=1", 32 | }) 33 | 34 | type LoaderData = { 35 | user: User 36 | } 37 | 38 | export const loader: LoaderFunction = async ({ request }) => { 39 | const res = await getUser(request) 40 | const user: User = res?.data 41 | const data: LoaderData = { 42 | user, 43 | } 44 | return json(data) 45 | } 46 | 47 | export default function App() { 48 | const data = useLoaderData() 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ) 60 | } 61 | 62 | function Document({ 63 | children, 64 | title = `Remix: So great, it's funny!`, 65 | }: { 66 | children: React.ReactNode 67 | title?: string 68 | }) { 69 | return ( 70 | 71 | 72 | 73 | {title} 74 | 75 | 76 | 77 | {children} 78 | 79 | 80 | 81 | 82 | 83 | ) 84 | } 85 | 86 | export function CatchBoundary() { 87 | const caught = useCatch() 88 | 89 | return ( 90 | 91 |
92 |

93 | {caught.status} {caught.statusText} 94 |

95 |
96 |
97 | ) 98 | } 99 | 100 | // 60 101 | export function ErrorBoundary({ error }: { error: Error }) { 102 | return ( 103 | 104 |
105 |

App Error

106 |
{error.message}
107 |
108 |
109 | ) 110 | } 111 | -------------------------------------------------------------------------------- /frontend_remix/app/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id: number 3 | attributes: { 4 | name: string 5 | products?: ProductType 6 | } 7 | } 8 | export interface Categories { 9 | data: Category[] 10 | } 11 | 12 | export interface ProductImageFormatProperties { 13 | name: string 14 | hash: string 15 | ext: string 16 | mime: string 17 | path: string 18 | width: number 19 | height: number 20 | size: number 21 | url: string 22 | } 23 | 24 | export interface ProductImageFormat { 25 | thumbnail: ProductImageFormatProperties 26 | medium: ProductImageFormatProperties 27 | small: ProductImageFormatProperties 28 | } 29 | export interface ProductImage { 30 | data: { 31 | id: number 32 | attributes: { 33 | name: string 34 | width: number 35 | height: number 36 | formats: ProductImageFormat 37 | hash: string 38 | ext: string 39 | mime: string 40 | size: number 41 | url: string 42 | previewUrl: string 43 | provider: string 44 | } 45 | } 46 | } 47 | export interface ProductStoreProperties { 48 | id: number 49 | attributes: { 50 | name: string 51 | address: string 52 | products?: ProductType 53 | } 54 | } 55 | export interface ProductStore { 56 | data: ProductStoreProperties 57 | } 58 | 59 | export interface ProductStores { 60 | data: ProductStoreProperties[] 61 | } 62 | 63 | export interface Product { 64 | id: number 65 | attributes: { 66 | name: string 67 | price: number 68 | quantity: number 69 | description: string 70 | categories: Categories 71 | image: ProductImage 72 | store: ProductStore 73 | } 74 | } 75 | export interface ProductType { 76 | data: Product[] 77 | } 78 | 79 | // User types 80 | export interface UserRole { 81 | id: number 82 | name: string 83 | description: string 84 | type: string 85 | } 86 | export interface UserAvatarFormatProperties { 87 | name: string 88 | hash: string 89 | ext: string 90 | mime: string 91 | path: null 92 | width: number 93 | height: number 94 | size: number 95 | url: string 96 | } 97 | export interface UserAvatarFormats { 98 | thumbnail: UserAvatarFormatProperties 99 | small: UserAvatarFormatProperties 100 | } 101 | export interface UserAvatar { 102 | id: number 103 | name: string 104 | width: number 105 | height: number 106 | formats: UserAvatarFormats 107 | hash: string 108 | ext: string 109 | mime: string 110 | size: number 111 | url: string 112 | previewUrl: null 113 | } 114 | export interface User { 115 | id: number 116 | username: string 117 | email: string 118 | confirmed: boolean 119 | blocked: boolean 120 | role: UserRole 121 | avatar: UserAvatar 122 | } 123 | 124 | export interface Sale { 125 | id: number 126 | attributes: { 127 | createdAt: string 128 | updatedAt: string 129 | quantity: number 130 | product: { 131 | data: Product 132 | } 133 | } 134 | } 135 | export interface Sales { 136 | data: Sale[] 137 | } 138 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/files.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /// JSON fixture file can be loaded directly using 4 | // the built-in JavaScript bundler 5 | const requiredExample = require("../../fixtures/example") 6 | 7 | context("Files", () => { 8 | beforeEach(() => { 9 | cy.visit("https://example.cypress.io/commands/files") 10 | }) 11 | 12 | beforeEach(() => { 13 | // load example.json fixture file and store 14 | // in the test context object 15 | cy.fixture("example.json").as("example") 16 | }) 17 | 18 | it("cy.fixture() - load a fixture", () => { 19 | // https://on.cypress.io/fixture 20 | 21 | // Instead of writing a response inline you can 22 | // use a fixture file's content. 23 | 24 | // when application makes an Ajax request matching "GET **/comments/*" 25 | // Cypress will intercept it and reply with the object in `example.json` fixture 26 | cy.intercept("GET", "**/comments/*", { fixture: "example.json" }).as( 27 | "getComment" 28 | ) 29 | 30 | // we have code that gets a comment when 31 | // the button is clicked in scripts.js 32 | cy.get(".fixture-btn").click() 33 | 34 | cy.wait("@getComment") 35 | .its("response.body") 36 | .should("have.property", "name") 37 | .and("include", "Using fixtures to represent data") 38 | }) 39 | 40 | it("cy.fixture() or require - load a fixture", function () { 41 | // we are inside the "function () { ... }" 42 | // callback and can use test context object "this" 43 | // "this.example" was loaded in "beforeEach" function callback 44 | expect(this.example, "fixture in the test context").to.deep.equal( 45 | requiredExample 46 | ) 47 | 48 | // or use "cy.wrap" and "should('deep.equal', ...)" assertion 49 | cy.wrap(this.example).should("deep.equal", requiredExample) 50 | }) 51 | 52 | it("cy.readFile() - read file contents", () => { 53 | // https://on.cypress.io/readfile 54 | 55 | // You can read a file and yield its contents 56 | // The filePath is relative to your project's root. 57 | cy.readFile(Cypress.config("configFile")).then((config) => { 58 | expect(config).to.be.an("string") 59 | }) 60 | }) 61 | 62 | it("cy.writeFile() - write to a file", () => { 63 | // https://on.cypress.io/writefile 64 | 65 | // You can write to a file 66 | 67 | // Use a response from a request to automatically 68 | // generate a fixture file for use later 69 | cy.request("https://jsonplaceholder.cypress.io/users").then( 70 | (response) => { 71 | cy.writeFile("cypress/fixtures/users.json", response.body) 72 | } 73 | ) 74 | 75 | cy.fixture("users").should((users) => { 76 | expect(users[0].name).to.exist 77 | }) 78 | 79 | // JavaScript arrays and objects are stringified 80 | // and formatted into text. 81 | cy.writeFile("cypress/fixtures/profile.json", { 82 | id: 8739, 83 | name: "Jane", 84 | email: "jane@example.com", 85 | }) 86 | 87 | cy.fixture("profile").should((profile) => { 88 | expect(profile.name).to.eq("Jane") 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /frontend_remix/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { PassThrough } from "stream" 2 | import type { EntryContext } from "@remix-run/node" 3 | import { Response } from "@remix-run/node" 4 | import { RemixServer } from "@remix-run/react" 5 | import isbot from "isbot" 6 | import { renderToPipeableStream } from "react-dom/server" 7 | 8 | require("dotenv").config() 9 | 10 | const ABORT_DELAY = 5000 11 | 12 | export default function handleRequest( 13 | request: Request, 14 | responseStatusCode: number, 15 | responseHeaders: Headers, 16 | remixContext: EntryContext 17 | ) { 18 | return isbot(request.headers.get("user-agent")) 19 | ? handleBotRequest( 20 | request, 21 | responseStatusCode, 22 | responseHeaders, 23 | remixContext 24 | ) 25 | : handleBrowserRequest( 26 | request, 27 | responseStatusCode, 28 | responseHeaders, 29 | remixContext 30 | ) 31 | } 32 | 33 | function handleBotRequest( 34 | request: Request, 35 | responseStatusCode: number, 36 | responseHeaders: Headers, 37 | remixContext: EntryContext 38 | ) { 39 | return new Promise((resolve, reject) => { 40 | let didError = false 41 | 42 | const { pipe, abort } = renderToPipeableStream( 43 | , 44 | { 45 | onAllReady() { 46 | const body = new PassThrough() 47 | 48 | responseHeaders.set("Content-Type", "text/html") 49 | 50 | resolve( 51 | new Response(body, { 52 | headers: responseHeaders, 53 | status: didError ? 500 : responseStatusCode, 54 | }) 55 | ) 56 | 57 | pipe(body) 58 | }, 59 | onShellError(error: unknown) { 60 | reject(error) 61 | }, 62 | onError(error: unknown) { 63 | didError = true 64 | 65 | console.error(error) 66 | }, 67 | } 68 | ) 69 | 70 | setTimeout(abort, ABORT_DELAY) 71 | }) 72 | } 73 | 74 | function handleBrowserRequest( 75 | request: Request, 76 | responseStatusCode: number, 77 | responseHeaders: Headers, 78 | remixContext: EntryContext 79 | ) { 80 | return new Promise((resolve, reject) => { 81 | let didError = false 82 | 83 | const { pipe, abort } = renderToPipeableStream( 84 | , 85 | { 86 | onShellReady() { 87 | const body = new PassThrough() 88 | 89 | responseHeaders.set("Content-Type", "text/html") 90 | 91 | resolve( 92 | new Response(body, { 93 | headers: responseHeaders, 94 | status: didError ? 500 : responseStatusCode, 95 | }) 96 | ) 97 | 98 | pipe(body) 99 | }, 100 | onShellError(err: unknown) { 101 | reject(err) 102 | }, 103 | onError(error: unknown) { 104 | didError = true 105 | 106 | console.error(error) 107 | }, 108 | } 109 | ) 110 | 111 | setTimeout(abort, ABORT_DELAY) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/connectors.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Connectors", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/connectors") 6 | }) 7 | 8 | it(".each() - iterate over an array of elements", () => { 9 | // https://on.cypress.io/each 10 | cy.get(".connectors-each-ul>li").each(($el, index, $list) => { 11 | console.log($el, index, $list) 12 | }) 13 | }) 14 | 15 | it(".its() - get properties on the current subject", () => { 16 | // https://on.cypress.io/its 17 | cy.get(".connectors-its-ul>li") 18 | // calls the 'length' property yielding that value 19 | .its("length") 20 | .should("be.gt", 2) 21 | }) 22 | 23 | it(".invoke() - invoke a function on the current subject", () => { 24 | // our div is hidden in our script.js 25 | // $('.connectors-div').hide() 26 | 27 | // https://on.cypress.io/invoke 28 | cy.get(".connectors-div") 29 | .should("be.hidden") 30 | // call the jquery method 'show' on the 'div.container' 31 | .invoke("show") 32 | .should("be.visible") 33 | }) 34 | 35 | it(".spread() - spread an array as individual args to callback function", () => { 36 | // https://on.cypress.io/spread 37 | const arr = ["foo", "bar", "baz"] 38 | 39 | cy.wrap(arr).spread((foo, bar, baz) => { 40 | expect(foo).to.eq("foo") 41 | expect(bar).to.eq("bar") 42 | expect(baz).to.eq("baz") 43 | }) 44 | }) 45 | 46 | describe(".then()", () => { 47 | it("invokes a callback function with the current subject", () => { 48 | // https://on.cypress.io/then 49 | cy.get(".connectors-list > li").then(($lis) => { 50 | expect($lis, "3 items").to.have.length(3) 51 | expect($lis.eq(0), "first item").to.contain("Walk the dog") 52 | expect($lis.eq(1), "second item").to.contain("Feed the cat") 53 | expect($lis.eq(2), "third item").to.contain("Write JavaScript") 54 | }) 55 | }) 56 | 57 | it("yields the returned value to the next command", () => { 58 | cy.wrap(1) 59 | .then((num) => { 60 | expect(num).to.equal(1) 61 | 62 | return 2 63 | }) 64 | .then((num) => { 65 | expect(num).to.equal(2) 66 | }) 67 | }) 68 | 69 | it("yields the original subject without return", () => { 70 | cy.wrap(1) 71 | .then((num) => { 72 | expect(num).to.equal(1) 73 | // note that nothing is returned from this callback 74 | }) 75 | .then((num) => { 76 | // this callback receives the original unchanged value 1 77 | expect(num).to.equal(1) 78 | }) 79 | }) 80 | 81 | it("yields the value yielded by the last Cypress command inside", () => { 82 | cy.wrap(1) 83 | .then((num) => { 84 | expect(num).to.equal(1) 85 | // note how we run a Cypress command 86 | // the result yielded by this Cypress command 87 | // will be passed to the second ".then" 88 | cy.wrap(2) 89 | }) 90 | .then((num) => { 91 | // this callback receives the value yielded by "cy.wrap(2)" 92 | expect(num).to.equal(2) 93 | }) 94 | }) 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /frontend_remix/test/unit/loginForm.test.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionData } from "~/routes/login" 2 | import type { Transition } from "@remix-run/react/dist/transition" 3 | import LoginForm from "~/components/Login/LoginForm" 4 | import { render, screen } from "@testing-library/react" 5 | import { useSearchParams } from "@remix-run/react" 6 | 7 | let searchP = {} as URLSearchParams 8 | 9 | // UI setup function 10 | function Setup({ 11 | actionData = {}, 12 | transition = { state: "idle" }, 13 | }: { 14 | actionData?: ActionData | undefined 15 | transition?: Pick 16 | }) { 17 | const utils = render( 18 | 23 | ) 24 | return { ...utils } 25 | } 26 | 27 | // Mocking remix-run/react module 28 | vi.mock("@remix-run/react", async () => { 29 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 30 | const actual = await vi.importActual( 31 | "@remix-run/react" 32 | ) 33 | const Form = ({ children }: { children: React.ReactNode }) => ( 34 |
{children}
35 | ) 36 | const useSearchParams = () => [{ get: vi.fn() }] 37 | return { ...actual, useSearchParams, Form } 38 | }) 39 | 40 | beforeAll(() => { 41 | const [searchParams] = useSearchParams() 42 | searchP = searchParams 43 | }) 44 | beforeEach(() => { 45 | vi.clearAllMocks() 46 | }) 47 | afterAll(() => { 48 | vi.restoreAllMocks() 49 | }) 50 | 51 | // Test 1 - Renders login form 52 | test("renders login form", async () => { 53 | // Setup UI 54 | Setup({}) 55 | const emailField = screen.getByLabelText(/email/i) 56 | const passwordField = screen.getByLabelText(/password/i) 57 | const loginBtn = screen.getByRole("button", { name: /log in/i }) 58 | expect(loginBtn).toBeInTheDocument() 59 | expect(emailField).toBeInTheDocument() 60 | expect(passwordField).toBeInTheDocument() 61 | 62 | // check if searchParams.get is called 63 | expect(searchP.get).toHaveBeenCalled() 64 | expect(searchP.get).toHaveBeenCalledTimes(1) 65 | }) 66 | // Test 2 - Renders login form with loading state 67 | test("login form loading state", async () => { 68 | // Setup UI 69 | Setup({ transition: { state: "loading" } }) 70 | // screen.debug() 71 | const loadingSvg = screen.getByLabelText(/Loading/i) 72 | expect(loadingSvg).toBeInTheDocument() 73 | 74 | // check if searchParams.get is called 75 | expect(searchP.get).toHaveBeenCalled() 76 | expect(searchP.get).toHaveBeenCalledTimes(1) 77 | }) 78 | // Test 3 - Renders login form with error state 79 | test("login form errors state", async () => { 80 | const emailErrorMsg = "Invalid email address" 81 | const passErrorMsg = "Passwords must be at least 6 characters long" 82 | const formErrorMsg = "Username/Password combination is incorrect" 83 | let actionData = { 84 | fieldErrors: { 85 | email: emailErrorMsg, 86 | password: passErrorMsg, 87 | }, 88 | formError: formErrorMsg, 89 | } as ActionData | undefined 90 | 91 | // Setup UI 92 | Setup({ actionData }) 93 | // screen.debug() 94 | const emailError = screen.getByText(emailErrorMsg) 95 | const passError = screen.getByText(passErrorMsg) 96 | const formError = screen.getByText(formErrorMsg) 97 | expect(emailError).toHaveTextContent(emailErrorMsg) 98 | expect(passError).toHaveTextContent(passErrorMsg) 99 | expect(formError).toHaveTextContent(formErrorMsg) 100 | 101 | // check if searchParams.get is called 102 | expect(searchP.get).toHaveBeenCalled() 103 | expect(searchP.get).toHaveBeenCalledTimes(1) 104 | }) 105 | -------------------------------------------------------------------------------- /frontend_remix/app/utils/session.server.ts: -------------------------------------------------------------------------------- 1 | import { createCookieSessionStorage, redirect } from "@remix-run/node" 2 | import axios from "axios" 3 | 4 | type LoginForm = { 5 | email: string 6 | password: string 7 | } 8 | 9 | export async function login({ email, password }: LoginForm) { 10 | try { 11 | const res = await axios.post("http://localhost:1337/api/auth/local", { 12 | identifier: email, 13 | password, 14 | }) 15 | const user = res.data 16 | // console.log("user ", user) 17 | if (!user) return null 18 | 19 | return user 20 | } catch (error) { 21 | return null 22 | } 23 | // return { id: user.id, username } 24 | } 25 | 26 | const sessionSecret = process.env.SESSION_SECRET 27 | if (!sessionSecret) { 28 | throw new Error("SESSION_SECRET must be set") 29 | } 30 | 31 | const storage = createCookieSessionStorage({ 32 | cookie: { 33 | name: "RJ_session", 34 | // normally you want this to be `secure: true` 35 | // but that doesn't work on localhost for Safari 36 | // https://web.dev/when-to-use-local-https/ 37 | secure: process.env.NODE_ENV === "production", 38 | secrets: [sessionSecret], 39 | sameSite: "lax", 40 | path: "/", 41 | maxAge: 60 * 60 * 24 * 30, 42 | httpOnly: true, 43 | }, 44 | }) 45 | 46 | function getUserSession(request: Request) { 47 | return storage.getSession(request.headers.get("Cookie")) 48 | } 49 | 50 | export async function getUserId(request: Request) { 51 | const session = await getUserSession(request) 52 | const userId = session.get("userId") 53 | if (!userId || typeof userId !== "number") return null 54 | return userId 55 | } 56 | export async function getUserJwt(request: Request) { 57 | const session = await getUserSession(request) 58 | const jwt = session.get("jwt") 59 | if (!jwt || typeof jwt !== "string") return null 60 | return jwt 61 | } 62 | export async function requireUserId( 63 | request: Request, 64 | redirectTo: string = new URL(request.url).pathname 65 | ) { 66 | const session = await getUserSession(request) 67 | // console.log("session ", session) 68 | const userId = session.get("userId") 69 | // console.log("userId ", userId) 70 | if (!userId || typeof userId !== "number") { 71 | const searchParams = new URLSearchParams([["redirectTo", redirectTo]]) 72 | throw redirect(`/login?${searchParams}`) 73 | } 74 | return userId 75 | } 76 | 77 | export async function getUser(request: Request) { 78 | const jwt = await getUserJwt(request) 79 | if (typeof jwt !== "string") { 80 | return null 81 | } 82 | 83 | try { 84 | const user = await axios.get( 85 | `http://localhost:1337/api/users/me?populate=role,avatar`, 86 | { 87 | headers: { 88 | Authorization: `Bearer ${jwt}`, 89 | }, 90 | } 91 | ) 92 | return user 93 | } catch { 94 | throw logout(request) 95 | } 96 | } 97 | 98 | export async function logout(request: Request) { 99 | const session = await getUserSession(request) 100 | return redirect("/login", { 101 | headers: { 102 | "Set-Cookie": await storage.destroySession(session), 103 | }, 104 | }) 105 | } 106 | 107 | export async function createUserSession( 108 | userId: string, 109 | jwt: string, 110 | redirectTo: string 111 | ) { 112 | const session = await storage.getSession() 113 | session.set("userId", userId) 114 | session.set("jwt", jwt) 115 | // console.log("session ", session) 116 | return redirect(redirectTo, { 117 | headers: { 118 | "Set-Cookie": await storage.commitSession(session), 119 | }, 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/misc.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Misc", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/misc") 6 | }) 7 | 8 | it(".end() - end the command chain", () => { 9 | // https://on.cypress.io/end 10 | 11 | // cy.end is useful when you want to end a chain of commands 12 | // and force Cypress to re-query from the root element 13 | cy.get(".misc-table").within(() => { 14 | // ends the current chain and yields null 15 | cy.contains("Cheryl").click().end() 16 | 17 | // queries the entire table again 18 | cy.contains("Charles").click() 19 | }) 20 | }) 21 | 22 | it("cy.exec() - execute a system command", () => { 23 | // execute a system command. 24 | // so you can take actions necessary for 25 | // your test outside the scope of Cypress. 26 | // https://on.cypress.io/exec 27 | 28 | // we can use Cypress.platform string to 29 | // select appropriate command 30 | // https://on.cypress/io/platform 31 | cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) 32 | 33 | // on CircleCI Windows build machines we have a failure to run bash shell 34 | // https://github.com/cypress-io/cypress/issues/5169 35 | // so skip some of the tests by passing flag "--env circle=true" 36 | const isCircleOnWindows = 37 | Cypress.platform === "win32" && Cypress.env("circle") 38 | 39 | if (isCircleOnWindows) { 40 | cy.log("Skipping test on CircleCI") 41 | 42 | return 43 | } 44 | 45 | // cy.exec problem on Shippable CI 46 | // https://github.com/cypress-io/cypress/issues/6718 47 | const isShippable = 48 | Cypress.platform === "linux" && Cypress.env("shippable") 49 | 50 | if (isShippable) { 51 | cy.log("Skipping test on ShippableCI") 52 | 53 | return 54 | } 55 | 56 | cy.exec("echo Jane Lane").its("stdout").should("contain", "Jane Lane") 57 | 58 | if (Cypress.platform === "win32") { 59 | cy.exec(`print ${Cypress.config("configFile")}`) 60 | .its("stderr") 61 | .should("be.empty") 62 | } else { 63 | cy.exec(`cat ${Cypress.config("configFile")}`) 64 | .its("stderr") 65 | .should("be.empty") 66 | 67 | cy.exec("pwd").its("code").should("eq", 0) 68 | } 69 | }) 70 | 71 | it("cy.focused() - get the DOM element that has focus", () => { 72 | // https://on.cypress.io/focused 73 | cy.get(".misc-form").find("#name").click() 74 | cy.focused().should("have.id", "name") 75 | 76 | cy.get(".misc-form").find("#description").click() 77 | cy.focused().should("have.id", "description") 78 | }) 79 | 80 | context("Cypress.Screenshot", function () { 81 | it("cy.screenshot() - take a screenshot", () => { 82 | // https://on.cypress.io/screenshot 83 | cy.screenshot("my-image") 84 | }) 85 | 86 | it("Cypress.Screenshot.defaults() - change default config of screenshots", function () { 87 | Cypress.Screenshot.defaults({ 88 | blackout: [".foo"], 89 | capture: "viewport", 90 | clip: { x: 0, y: 0, width: 200, height: 200 }, 91 | scale: false, 92 | disableTimersAndAnimations: true, 93 | screenshotOnRunFailure: true, 94 | onBeforeScreenshot() {}, 95 | onAfterScreenshot() {}, 96 | }) 97 | }) 98 | }) 99 | 100 | it("cy.wrap() - wrap an object", () => { 101 | // https://on.cypress.io/wrap 102 | cy.wrap({ foo: "bar" }) 103 | .should("have.property", "foo") 104 | .and("include", "bar") 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/querying.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Querying", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/querying") 6 | }) 7 | 8 | // The most commonly used query is 'cy.get()', you can 9 | // think of this like the '$' in jQuery 10 | 11 | it("cy.get() - query DOM elements", () => { 12 | // https://on.cypress.io/get 13 | 14 | cy.get("#query-btn").should("contain", "Button") 15 | 16 | cy.get(".query-btn").should("contain", "Button") 17 | 18 | cy.get("#querying .well>button:first").should("contain", "Button") 19 | // ↲ 20 | // Use CSS selectors just like jQuery 21 | 22 | cy.get('[data-test-id="test-example"]').should("have.class", "example") 23 | 24 | // 'cy.get()' yields jQuery object, you can get its attribute 25 | // by invoking `.attr()` method 26 | cy.get('[data-test-id="test-example"]') 27 | .invoke("attr", "data-test-id") 28 | .should("equal", "test-example") 29 | 30 | // or you can get element's CSS property 31 | cy.get('[data-test-id="test-example"]') 32 | .invoke("css", "position") 33 | .should("equal", "static") 34 | 35 | // or use assertions directly during 'cy.get()' 36 | // https://on.cypress.io/assertions 37 | cy.get('[data-test-id="test-example"]') 38 | .should("have.attr", "data-test-id", "test-example") 39 | .and("have.css", "position", "static") 40 | }) 41 | 42 | it("cy.contains() - query DOM elements with matching content", () => { 43 | // https://on.cypress.io/contains 44 | cy.get(".query-list").contains("bananas").should("have.class", "third") 45 | 46 | // we can pass a regexp to `.contains()` 47 | cy.get(".query-list").contains(/^b\w+/).should("have.class", "third") 48 | 49 | cy.get(".query-list").contains("apples").should("have.class", "first") 50 | 51 | // passing a selector to contains will 52 | // yield the selector containing the text 53 | cy.get("#querying") 54 | .contains("ul", "oranges") 55 | .should("have.class", "query-list") 56 | 57 | cy.get(".query-button") 58 | .contains("Save Form") 59 | .should("have.class", "btn") 60 | }) 61 | 62 | it(".within() - query DOM elements within a specific element", () => { 63 | // https://on.cypress.io/within 64 | cy.get(".query-form").within(() => { 65 | cy.get("input:first").should("have.attr", "placeholder", "Email") 66 | cy.get("input:last").should("have.attr", "placeholder", "Password") 67 | }) 68 | }) 69 | 70 | it("cy.root() - query the root DOM element", () => { 71 | // https://on.cypress.io/root 72 | 73 | // By default, root is the document 74 | cy.root().should("match", "html") 75 | 76 | cy.get(".query-ul").within(() => { 77 | // In this within, the root is now the ul DOM element 78 | cy.root().should("have.class", "query-ul") 79 | }) 80 | }) 81 | 82 | it("best practices - selecting elements", () => { 83 | // https://on.cypress.io/best-practices#Selecting-Elements 84 | cy.get("[data-cy=best-practices-selecting-elements]").within(() => { 85 | // Worst - too generic, no context 86 | cy.get("button").click() 87 | 88 | // Bad. Coupled to styling. Highly subject to change. 89 | cy.get(".btn.btn-large").click() 90 | 91 | // Average. Coupled to the `name` attribute which has HTML semantics. 92 | cy.get("[name=submission]").click() 93 | 94 | // Better. But still coupled to styling or JS event listeners. 95 | cy.get("#main").click() 96 | 97 | // Slightly better. Uses an ID but also ensures the element 98 | // has an ARIA role attribute 99 | cy.get("#main[role=button]").click() 100 | 101 | // Much better. But still coupled to text content that may change. 102 | cy.contains("Submit").click() 103 | 104 | // Best. Insulated from all changes. 105 | cy.get("[data-cy=submit]").click() 106 | }) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/traversal.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Traversal", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/traversal") 6 | }) 7 | 8 | it(".children() - get child DOM elements", () => { 9 | // https://on.cypress.io/children 10 | cy.get(".traversal-breadcrumb") 11 | .children(".active") 12 | .should("contain", "Data") 13 | }) 14 | 15 | it(".closest() - get closest ancestor DOM element", () => { 16 | // https://on.cypress.io/closest 17 | cy.get(".traversal-badge") 18 | .closest("ul") 19 | .should("have.class", "list-group") 20 | }) 21 | 22 | it(".eq() - get a DOM element at a specific index", () => { 23 | // https://on.cypress.io/eq 24 | cy.get(".traversal-list>li").eq(1).should("contain", "siamese") 25 | }) 26 | 27 | it(".filter() - get DOM elements that match the selector", () => { 28 | // https://on.cypress.io/filter 29 | cy.get(".traversal-nav>li").filter(".active").should("contain", "About") 30 | }) 31 | 32 | it(".find() - get descendant DOM elements of the selector", () => { 33 | // https://on.cypress.io/find 34 | cy.get(".traversal-pagination") 35 | .find("li") 36 | .find("a") 37 | .should("have.length", 7) 38 | }) 39 | 40 | it(".first() - get first DOM element", () => { 41 | // https://on.cypress.io/first 42 | cy.get(".traversal-table td").first().should("contain", "1") 43 | }) 44 | 45 | it(".last() - get last DOM element", () => { 46 | // https://on.cypress.io/last 47 | cy.get(".traversal-buttons .btn").last().should("contain", "Submit") 48 | }) 49 | 50 | it(".next() - get next sibling DOM element", () => { 51 | // https://on.cypress.io/next 52 | cy.get(".traversal-ul") 53 | .contains("apples") 54 | .next() 55 | .should("contain", "oranges") 56 | }) 57 | 58 | it(".nextAll() - get all next sibling DOM elements", () => { 59 | // https://on.cypress.io/nextall 60 | cy.get(".traversal-next-all") 61 | .contains("oranges") 62 | .nextAll() 63 | .should("have.length", 3) 64 | }) 65 | 66 | it(".nextUntil() - get next sibling DOM elements until next el", () => { 67 | // https://on.cypress.io/nextuntil 68 | cy.get("#veggies").nextUntil("#nuts").should("have.length", 3) 69 | }) 70 | 71 | it(".not() - remove DOM elements from set of DOM elements", () => { 72 | // https://on.cypress.io/not 73 | cy.get(".traversal-disabled .btn") 74 | .not("[disabled]") 75 | .should("not.contain", "Disabled") 76 | }) 77 | 78 | it(".parent() - get parent DOM element from DOM elements", () => { 79 | // https://on.cypress.io/parent 80 | cy.get(".traversal-mark").parent().should("contain", "Morbi leo risus") 81 | }) 82 | 83 | it(".parents() - get parent DOM elements from DOM elements", () => { 84 | // https://on.cypress.io/parents 85 | cy.get(".traversal-cite").parents().should("match", "blockquote") 86 | }) 87 | 88 | it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => { 89 | // https://on.cypress.io/parentsuntil 90 | cy.get(".clothes-nav") 91 | .find(".active") 92 | .parentsUntil(".clothes-nav") 93 | .should("have.length", 2) 94 | }) 95 | 96 | it(".prev() - get previous sibling DOM element", () => { 97 | // https://on.cypress.io/prev 98 | cy.get(".birds").find(".active").prev().should("contain", "Lorikeets") 99 | }) 100 | 101 | it(".prevAll() - get all previous sibling DOM elements", () => { 102 | // https://on.cypress.io/prevall 103 | cy.get(".fruits-list").find(".third").prevAll().should("have.length", 2) 104 | }) 105 | 106 | it(".prevUntil() - get all previous sibling DOM elements until el", () => { 107 | // https://on.cypress.io/prevuntil 108 | cy.get(".foods-list") 109 | .find("#nuts") 110 | .prevUntil("#veggies") 111 | .should("have.length", 3) 112 | }) 113 | 114 | it(".siblings() - get all sibling DOM elements", () => { 115 | // https://on.cypress.io/siblings 116 | cy.get(".traversal-pills .active").siblings().should("have.length", 2) 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /frontend_remix/app/components/Login/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionData } from "~/routes/login" 2 | import type { Transition } from "@remix-run/react/dist/transition" 3 | import { Form } from "@remix-run/react" 4 | import Button from "../Button" 5 | import { AiOutlineLoading3Quarters } from "react-icons/ai" 6 | 7 | function LoginForm({ 8 | actionData, 9 | searchParams, 10 | transition, 11 | }: { 12 | actionData: ActionData | undefined 13 | searchParams: URLSearchParams 14 | transition: Pick 15 | }) { 16 | return ( 17 |
18 | 23 | 48 | 76 | 77 |
78 | {actionData?.formError ? ( 79 |

80 | {actionData.formError} 81 |

82 | ) : null} 83 |
84 | {/* */} 85 | 100 |
101 | ) 102 | } 103 | 104 | export default LoginForm 105 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/utilities.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Utilities", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/utilities") 6 | }) 7 | 8 | it("Cypress._ - call a lodash method", () => { 9 | // https://on.cypress.io/_ 10 | cy.request("https://jsonplaceholder.cypress.io/users").then( 11 | (response) => { 12 | let ids = Cypress._.chain(response.body) 13 | .map("id") 14 | .take(3) 15 | .value() 16 | 17 | expect(ids).to.deep.eq([1, 2, 3]) 18 | } 19 | ) 20 | }) 21 | 22 | it("Cypress.$ - call a jQuery method", () => { 23 | // https://on.cypress.io/$ 24 | let $li = Cypress.$(".utility-jquery li:first") 25 | 26 | cy.wrap($li) 27 | .should("not.have.class", "active") 28 | .click() 29 | .should("have.class", "active") 30 | }) 31 | 32 | it("Cypress.Blob - blob utilities and base64 string conversion", () => { 33 | // https://on.cypress.io/blob 34 | cy.get(".utility-blob").then(($div) => { 35 | // https://github.com/nolanlawson/blob-util#imgSrcToDataURL 36 | // get the dataUrl string for the javascript-logo 37 | return Cypress.Blob.imgSrcToDataURL( 38 | "https://example.cypress.io/assets/img/javascript-logo.png", 39 | undefined, 40 | "anonymous" 41 | ).then((dataUrl) => { 42 | // create an element and set its src to the dataUrl 43 | let img = Cypress.$("", { src: dataUrl }) 44 | 45 | // need to explicitly return cy here since we are initially returning 46 | // the Cypress.Blob.imgSrcToDataURL promise to our test 47 | // append the image 48 | $div.append(img) 49 | 50 | cy.get(".utility-blob img") 51 | .click() 52 | .should("have.attr", "src", dataUrl) 53 | }) 54 | }) 55 | }) 56 | 57 | it("Cypress.minimatch - test out glob patterns against strings", () => { 58 | // https://on.cypress.io/minimatch 59 | let matching = Cypress.minimatch( 60 | "/users/1/comments", 61 | "/users/*/comments", 62 | { 63 | matchBase: true, 64 | } 65 | ) 66 | 67 | expect(matching, "matching wildcard").to.be.true 68 | 69 | matching = Cypress.minimatch( 70 | "/users/1/comments/2", 71 | "/users/*/comments", 72 | { 73 | matchBase: true, 74 | } 75 | ) 76 | 77 | expect(matching, "comments").to.be.false 78 | 79 | // ** matches against all downstream path segments 80 | matching = Cypress.minimatch( 81 | "/foo/bar/baz/123/quux?a=b&c=2", 82 | "/foo/**", 83 | { 84 | matchBase: true, 85 | } 86 | ) 87 | 88 | expect(matching, "comments").to.be.true 89 | 90 | // whereas * matches only the next path segment 91 | 92 | matching = Cypress.minimatch( 93 | "/foo/bar/baz/123/quux?a=b&c=2", 94 | "/foo/*", 95 | { 96 | matchBase: false, 97 | } 98 | ) 99 | 100 | expect(matching, "comments").to.be.false 101 | }) 102 | 103 | it("Cypress.Promise - instantiate a bluebird promise", () => { 104 | // https://on.cypress.io/promise 105 | let waited = false 106 | 107 | /** 108 | * @return Bluebird 109 | */ 110 | function waitOneSecond() { 111 | // return a promise that resolves after 1 second 112 | return new Cypress.Promise((resolve, reject) => { 113 | setTimeout(() => { 114 | // set waited to true 115 | waited = true 116 | 117 | // resolve with 'foo' string 118 | resolve("foo") 119 | }, 1000) 120 | }) 121 | } 122 | 123 | cy.then(() => { 124 | // return a promise to cy.then() that 125 | // is awaited until it resolves 126 | return waitOneSecond().then((str) => { 127 | expect(str).to.eq("foo") 128 | expect(waited).to.be.true 129 | }) 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/storage.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Local Storage / Session Storage", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/storage") 6 | }) 7 | // Although localStorage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear localStorage manually 10 | 11 | it("cy.clearLocalStorage() - clear all data in localStorage for the current origin", () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get(".ls-btn") 14 | .click() 15 | .should(() => { 16 | expect(localStorage.getItem("prop1")).to.eq("red") 17 | expect(localStorage.getItem("prop2")).to.eq("blue") 18 | expect(localStorage.getItem("prop3")).to.eq("magenta") 19 | }) 20 | 21 | // clearLocalStorage() yields the localStorage object 22 | cy.clearLocalStorage().should((ls) => { 23 | expect(ls.getItem("prop1")).to.be.null 24 | expect(ls.getItem("prop2")).to.be.null 25 | expect(ls.getItem("prop3")).to.be.null 26 | }) 27 | 28 | cy.get(".ls-btn") 29 | .click() 30 | .should(() => { 31 | expect(localStorage.getItem("prop1")).to.eq("red") 32 | expect(localStorage.getItem("prop2")).to.eq("blue") 33 | expect(localStorage.getItem("prop3")).to.eq("magenta") 34 | }) 35 | 36 | // Clear key matching string in localStorage 37 | cy.clearLocalStorage("prop1").should((ls) => { 38 | expect(ls.getItem("prop1")).to.be.null 39 | expect(ls.getItem("prop2")).to.eq("blue") 40 | expect(ls.getItem("prop3")).to.eq("magenta") 41 | }) 42 | 43 | cy.get(".ls-btn") 44 | .click() 45 | .should(() => { 46 | expect(localStorage.getItem("prop1")).to.eq("red") 47 | expect(localStorage.getItem("prop2")).to.eq("blue") 48 | expect(localStorage.getItem("prop3")).to.eq("magenta") 49 | }) 50 | 51 | // Clear keys matching regex in localStorage 52 | cy.clearLocalStorage(/prop1|2/).should((ls) => { 53 | expect(ls.getItem("prop1")).to.be.null 54 | expect(ls.getItem("prop2")).to.be.null 55 | expect(ls.getItem("prop3")).to.eq("magenta") 56 | }) 57 | }) 58 | 59 | it("cy.getAllLocalStorage() - get all data in localStorage for all origins", () => { 60 | // https://on.cypress.io/getalllocalstorage 61 | cy.get(".ls-btn").click() 62 | 63 | // getAllLocalStorage() yields a map of origins to localStorage values 64 | cy.getAllLocalStorage().should((storageMap) => { 65 | expect(storageMap).to.deep.equal({ 66 | // other origins will also be present if localStorage is set on them 67 | "https://example.cypress.io": { 68 | prop1: "red", 69 | prop2: "blue", 70 | prop3: "magenta", 71 | }, 72 | }) 73 | }) 74 | }) 75 | 76 | it("cy.clearAllLocalStorage() - clear all data in localStorage for all origins", () => { 77 | // https://on.cypress.io/clearalllocalstorage 78 | cy.get(".ls-btn").click() 79 | 80 | // clearAllLocalStorage() yields null 81 | cy.clearAllLocalStorage().should(() => { 82 | expect(sessionStorage.getItem("prop1")).to.be.null 83 | expect(sessionStorage.getItem("prop2")).to.be.null 84 | expect(sessionStorage.getItem("prop3")).to.be.null 85 | }) 86 | }) 87 | 88 | it("cy.getAllSessionStorage() - get all data in sessionStorage for all origins", () => { 89 | // https://on.cypress.io/getallsessionstorage 90 | cy.get(".ls-btn").click() 91 | 92 | // getAllSessionStorage() yields a map of origins to sessionStorage values 93 | cy.getAllSessionStorage().should((storageMap) => { 94 | expect(storageMap).to.deep.equal({ 95 | // other origins will also be present if sessionStorage is set on them 96 | "https://example.cypress.io": { 97 | prop4: "cyan", 98 | prop5: "yellow", 99 | prop6: "black", 100 | }, 101 | }) 102 | }) 103 | }) 104 | 105 | it("cy.clearAllSessionStorage() - clear all data in sessionStorage for all origins", () => { 106 | // https://on.cypress.io/clearallsessionstorage 107 | cy.get(".ls-btn").click() 108 | 109 | // clearAllSessionStorage() yields null 110 | cy.clearAllSessionStorage().should(() => { 111 | expect(sessionStorage.getItem("prop4")).to.be.null 112 | expect(sessionStorage.getItem("prop5")).to.be.null 113 | expect(sessionStorage.getItem("prop6")).to.be.null 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ActionFunction, 3 | LoaderFunction, 4 | MetaFunction, 5 | } from "@remix-run/node" 6 | import { json, redirect } from "@remix-run/node" 7 | import { useActionData, useSearchParams, useTransition } from "@remix-run/react" 8 | import validator from "validator" 9 | import LoginForm from "~/components/Login/LoginForm" 10 | import { login, createUserSession, getUserId } from "~/utils/session.server" 11 | 12 | export const meta: MetaFunction = () => ({ 13 | title: "Login", 14 | }) 15 | 16 | function validateEmail(email: unknown) { 17 | if (typeof email !== "string" || !validator.isEmail(email)) { 18 | return `Invalid email address` 19 | } 20 | } 21 | 22 | function validatePassword(password: unknown) { 23 | if (typeof password !== "string" || password.length < 6) { 24 | return `Passwords must be at least 6 characters long` 25 | } 26 | } 27 | 28 | function validateUrl(url: any) { 29 | console.log(url) 30 | let urls = [ 31 | "/dashboard", 32 | "/products", 33 | "/products/list", 34 | "/products/add", 35 | "/", 36 | "https://remix.run", 37 | ] 38 | if (urls.includes(url)) { 39 | return url 40 | } 41 | return "/dashboard" 42 | } 43 | 44 | export type ActionData = { 45 | formError?: string 46 | fieldErrors?: { 47 | email?: string | undefined 48 | password?: string | undefined 49 | } 50 | fields?: { 51 | email?: string 52 | password?: string 53 | } 54 | } 55 | 56 | const badRequest = (data: ActionData) => json(data, { status: 400 }) 57 | 58 | export const action: ActionFunction = async ({ request }) => { 59 | const form = await request.formData() 60 | const email = form.get("identifier") 61 | const password = form.get("password") 62 | const redirectTo = validateUrl(form.get("redirectTo") || "/dashboard") 63 | if ( 64 | typeof email !== "string" || 65 | typeof password !== "string" || 66 | typeof redirectTo !== "string" 67 | ) { 68 | return badRequest({ 69 | formError: `Form not submitted correctly.`, 70 | }) 71 | } 72 | 73 | const fields = { email, password } 74 | const fieldErrors = { 75 | email: validateEmail(email), 76 | password: validatePassword(password), 77 | } 78 | if (Object.values(fieldErrors).some(Boolean)) 79 | return badRequest({ fieldErrors, fields }) 80 | 81 | const user = await login({ email, password }) 82 | // console.log({ user }) 83 | if (!user) { 84 | return badRequest({ 85 | fields, 86 | formError: `Username/Password combination is incorrect`, 87 | }) 88 | } 89 | return createUserSession(user.user.id, user.jwt, redirectTo) 90 | } 91 | 92 | export const loader: LoaderFunction = async ({ request }) => { 93 | const userId = await getUserId(request) 94 | if (userId) { 95 | return redirect("/dashboard") 96 | } 97 | return null 98 | } 99 | 100 | function Login() { 101 | const actionData = useActionData() 102 | const [searchParams] = useSearchParams() 103 | const transition = useTransition() 104 | return ( 105 |
106 |
107 |
108 |
109 | 115 | 121 |
122 |
123 |
124 |

125 | Login 126 |

127 | 132 |
133 |
134 |
135 |
136 |
137 | ) 138 | } 139 | 140 | export default Login 141 | -------------------------------------------------------------------------------- /frontend_remix/app/routes/categories/add.tsx: -------------------------------------------------------------------------------- 1 | import type { ActionFunction, MetaFunction } from "@remix-run/node" 2 | import { redirect, json } from "@remix-run/node" 3 | import { Form, useActionData, useTransition } from "@remix-run/react" 4 | import { AiOutlineLoading3Quarters } from "react-icons/ai" 5 | import type { AxiosError } from "axios" 6 | import axios from "axios" 7 | import Button from "~/components/Button" 8 | import config from "~/config" 9 | import { getUserJwt } from "~/utils/session.server" 10 | 11 | const SERVER_URL = config.SERVER_URL 12 | 13 | export const meta: MetaFunction = () => ({ 14 | title: "Add new product", 15 | }) 16 | 17 | function validateName(name: unknown) { 18 | if (typeof name !== "string" || name.length < 2) { 19 | return `Category name is too short` 20 | } 21 | } 22 | 23 | type ActionData = { 24 | formError?: string 25 | fieldErrors?: { 26 | name: string | undefined 27 | } 28 | fields?: { 29 | name: string 30 | } 31 | } 32 | const badRequest = (data: ActionData) => json(data, { status: 400 }) 33 | export const action: ActionFunction = async ({ request }) => { 34 | const jwt = await getUserJwt(request) 35 | const form = await request.formData() 36 | const name = form.get("name") 37 | if (typeof name !== "string") { 38 | return badRequest({ 39 | formError: `Form not submitted correctly.`, 40 | }) 41 | } 42 | 43 | const fields = { 44 | name, 45 | } 46 | const fieldErrors = { 47 | name: validateName(name), 48 | } 49 | 50 | if (Object.values(fieldErrors).some(Boolean)) 51 | return badRequest({ fieldErrors, fields }) 52 | 53 | try { 54 | await axios.post( 55 | `${SERVER_URL}/api/categories`, 56 | { 57 | data: { 58 | name, 59 | }, 60 | }, 61 | { 62 | headers: { 63 | Authorization: `Bearer ${jwt}`, 64 | }, 65 | } 66 | ) 67 | return redirect(`/categories/all`) 68 | } catch (error) { 69 | const err = error as AxiosError 70 | 71 | console.log(err.response?.data) 72 | return badRequest({ 73 | formError: "Something is wrong, please try again.", 74 | fields, 75 | }) 76 | } 77 | // return null 78 | } 79 | 80 | function Add() { 81 | const actionData = useActionData() 82 | const transition = useTransition() 83 | return ( 84 |
85 |
86 |

87 | New category 88 |

89 |
90 | {actionData?.formError ? ( 91 |

92 | {actionData.formError} 93 |

94 | ) : null} 95 |
96 |
101 |
102 | 126 |
127 |
128 | 142 |
143 |
144 |
145 |
146 | ) 147 | } 148 | 149 | export default Add 150 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/2-advanced-examples/cypress_api.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Cypress.Commands", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/cypress-api") 6 | }) 7 | 8 | // https://on.cypress.io/custom-commands 9 | 10 | it(".add() - create a custom command", () => { 11 | Cypress.Commands.add( 12 | "console", 13 | { 14 | prevSubject: true, 15 | }, 16 | (subject, method) => { 17 | // the previous subject is automatically received 18 | // and the commands arguments are shifted 19 | 20 | // allow us to change the console method used 21 | method = method || "log" 22 | 23 | // log the subject to the console 24 | console[method]("The subject is", subject) 25 | 26 | // whatever we return becomes the new subject 27 | // we don't want to change the subject so 28 | // we return whatever was passed in 29 | return subject 30 | } 31 | ) 32 | 33 | cy.get("button") 34 | .console("info") 35 | .then(($button) => { 36 | // subject is still $button 37 | }) 38 | }) 39 | }) 40 | 41 | context("Cypress.Cookies", () => { 42 | beforeEach(() => { 43 | cy.visit("https://example.cypress.io/cypress-api") 44 | }) 45 | 46 | // https://on.cypress.io/cookies 47 | it(".debug() - enable or disable debugging", () => { 48 | Cypress.Cookies.debug(true) 49 | 50 | // Cypress will now log in the console when 51 | // cookies are set or cleared 52 | cy.setCookie("fakeCookie", "123ABC") 53 | cy.clearCookie("fakeCookie") 54 | cy.setCookie("fakeCookie", "123ABC") 55 | cy.clearCookie("fakeCookie") 56 | cy.setCookie("fakeCookie", "123ABC") 57 | }) 58 | }) 59 | 60 | context("Cypress.arch", () => { 61 | beforeEach(() => { 62 | cy.visit("https://example.cypress.io/cypress-api") 63 | }) 64 | 65 | it("Get CPU architecture name of underlying OS", () => { 66 | // https://on.cypress.io/arch 67 | expect(Cypress.arch).to.exist 68 | }) 69 | }) 70 | 71 | context("Cypress.config()", () => { 72 | beforeEach(() => { 73 | cy.visit("https://example.cypress.io/cypress-api") 74 | }) 75 | 76 | it("Get and set configuration options", () => { 77 | // https://on.cypress.io/config 78 | let myConfig = Cypress.config() 79 | 80 | expect(myConfig).to.have.property("animationDistanceThreshold", 5) 81 | expect(myConfig).to.have.property("baseUrl", null) 82 | expect(myConfig).to.have.property("defaultCommandTimeout", 4000) 83 | expect(myConfig).to.have.property("requestTimeout", 5000) 84 | expect(myConfig).to.have.property("responseTimeout", 30000) 85 | expect(myConfig).to.have.property("viewportHeight", 660) 86 | expect(myConfig).to.have.property("viewportWidth", 1000) 87 | expect(myConfig).to.have.property("pageLoadTimeout", 60000) 88 | expect(myConfig).to.have.property("waitForAnimations", true) 89 | 90 | expect(Cypress.config("pageLoadTimeout")).to.eq(60000) 91 | 92 | // this will change the config for the rest of your tests! 93 | Cypress.config("pageLoadTimeout", 20000) 94 | 95 | expect(Cypress.config("pageLoadTimeout")).to.eq(20000) 96 | 97 | Cypress.config("pageLoadTimeout", 60000) 98 | }) 99 | }) 100 | 101 | context("Cypress.dom", () => { 102 | beforeEach(() => { 103 | cy.visit("https://example.cypress.io/cypress-api") 104 | }) 105 | 106 | // https://on.cypress.io/dom 107 | it(".isHidden() - determine if a DOM element is hidden", () => { 108 | let hiddenP = Cypress.$(".dom-p p.hidden").get(0) 109 | let visibleP = Cypress.$(".dom-p p.visible").get(0) 110 | 111 | // our first paragraph has css class 'hidden' 112 | expect(Cypress.dom.isHidden(hiddenP)).to.be.true 113 | expect(Cypress.dom.isHidden(visibleP)).to.be.false 114 | }) 115 | }) 116 | 117 | context("Cypress.env()", () => { 118 | beforeEach(() => { 119 | cy.visit("https://example.cypress.io/cypress-api") 120 | }) 121 | 122 | // We can set environment variables for highly dynamic values 123 | 124 | // https://on.cypress.io/environment-variables 125 | it("Get environment variables", () => { 126 | // https://on.cypress.io/env 127 | // set multiple environment variables 128 | Cypress.env({ 129 | host: "veronica.dev.local", 130 | api_server: "http://localhost:8888/v1/", 131 | }) 132 | 133 | // get environment variable 134 | expect(Cypress.env("host")).to.eq("veronica.dev.local") 135 | 136 | // set environment variable 137 | Cypress.env("api_server", "http://localhost:8888/v2/") 138 | expect(Cypress.env("api_server")).to.eq("http://localhost:8888/v2/") 139 | 140 | // get all environment variable 141 | expect(Cypress.env()).to.have.property("host", "veronica.dev.local") 142 | expect(Cypress.env()).to.have.property( 143 | "api_server", 144 | "http://localhost:8888/v2/" 145 | ) 146 | }) 147 | }) 148 | 149 | context("Cypress.log", () => { 150 | beforeEach(() => { 151 | cy.visit("https://example.cypress.io/cypress-api") 152 | }) 153 | 154 | it("Control what is printed to the Command Log", () => { 155 | // https://on.cypress.io/cypress-log 156 | }) 157 | }) 158 | 159 | context("Cypress.platform", () => { 160 | beforeEach(() => { 161 | cy.visit("https://example.cypress.io/cypress-api") 162 | }) 163 | 164 | it("Get underlying OS name", () => { 165 | // https://on.cypress.io/platform 166 | expect(Cypress.platform).to.be.exist 167 | }) 168 | }) 169 | 170 | context("Cypress.version", () => { 171 | beforeEach(() => { 172 | cy.visit("https://example.cypress.io/cypress-api") 173 | }) 174 | 175 | it("Get current version of Cypress being run", () => { 176 | // https://on.cypress.io/version 177 | expect(Cypress.version).to.be.exist 178 | }) 179 | }) 180 | 181 | context("Cypress.spec", () => { 182 | beforeEach(() => { 183 | cy.visit("https://example.cypress.io/cypress-api") 184 | }) 185 | 186 | it("Get current spec information", () => { 187 | // https://on.cypress.io/spec 188 | // wrap the object so we can inspect it easily by clicking in the command log 189 | cy.wrap(Cypress.spec).should("include.keys", [ 190 | "name", 191 | "relative", 192 | "absolute", 193 | ]) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /frontend_remix/cypress/e2e/1-getting-started/todo.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Welcome to Cypress! 4 | // 5 | // This spec file contains a variety of sample tests 6 | // for a todo list app that are designed to demonstrate 7 | // the power of writing tests in Cypress. 8 | // 9 | // To learn more about how Cypress works and 10 | // what makes it such an awesome testing tool, 11 | // please read our getting started guide: 12 | // https://on.cypress.io/introduction-to-cypress 13 | 14 | describe("example to-do app", () => { 15 | beforeEach(() => { 16 | // Cypress starts out with a blank slate for each test 17 | // so we must tell it to visit our website with the `cy.visit()` command. 18 | // Since we want to visit the same URL at the start of all our tests, 19 | // we include it in our beforeEach function so that it runs before each test 20 | cy.visit("https://example.cypress.io/todo") 21 | }) 22 | 23 | it("displays two todo items by default", () => { 24 | // We use the `cy.get()` command to get all elements that match the selector. 25 | // Then, we use `should` to assert that there are two matched items, 26 | // which are the two default items. 27 | cy.get(".todo-list li").should("have.length", 2) 28 | 29 | // We can go even further and check that the default todos each contain 30 | // the correct text. We use the `first` and `last` functions 31 | // to get just the first and last matched elements individually, 32 | // and then perform an assertion with `should`. 33 | cy.get(".todo-list li").first().should("have.text", "Pay electric bill") 34 | cy.get(".todo-list li").last().should("have.text", "Walk the dog") 35 | }) 36 | 37 | it("can add new todo items", () => { 38 | // We'll store our item text in a variable so we can reuse it 39 | const newItem = "Feed the cat" 40 | 41 | // Let's get the input element and use the `type` command to 42 | // input our new list item. After typing the content of our item, 43 | // we need to type the enter key as well in order to submit the input. 44 | // This input has a data-test attribute so we'll use that to select the 45 | // element in accordance with best practices: 46 | // https://on.cypress.io/selecting-elements 47 | cy.get("[data-test=new-todo]").type(`${newItem}{enter}`) 48 | 49 | // Now that we've typed our new item, let's check that it actually was added to the list. 50 | // Since it's the newest item, it should exist as the last element in the list. 51 | // In addition, with the two default items, we should have a total of 3 elements in the list. 52 | // Since assertions yield the element that was asserted on, 53 | // we can chain both of these assertions together into a single statement. 54 | cy.get(".todo-list li") 55 | .should("have.length", 3) 56 | .last() 57 | .should("have.text", newItem) 58 | }) 59 | 60 | it("can check off an item as completed", () => { 61 | // In addition to using the `get` command to get an element by selector, 62 | // we can also use the `contains` command to get an element by its contents. 63 | // However, this will yield the